Websockets, Play Framework and Actor - java

I need to inform all users about adding new Record to the database.
So, I have the following code
Application.java - here I placed socket handler method
public WebSocket<JsonNode> sockHandler() {
return WebSocket.withActor(ResponseActor::props);
}
Then I opened the connection
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var socket = new WS("#routes.Application.sockHandler().webSocketURL(request)")
socket.onmessage = function(event) {
console.log(event);
console.log(event.data);
console.log(event.responseJSON)
}});
My Actor class
public class ResponseActor extends UntypedActor {
private final ActorRef out;
public ResponseActor(ActorRef out) {
this.out = out;
}
public static Props props(ActorRef out) {
return Props.create(ResponseActor.class, out);
}
#Override
public void onReceive(Object response) throws Exception {
out.tell(Json.toJson(response), self());
}
}
And the last, as I think, I need to invoke the Actor from my Response Controller
public Result addPost() {
Map<String, String[]> request = request().body().asFormUrlEncoded();
Response response = new Response(request);
Map<String, String> validationMap = ResponseValidator.validate(response.responses);
if (validationMap.isEmpty()) {
ResponseDAO.create(response);
ActorRef responseActorRef = Akka.system().actorOf(ResponseActor.props(outRef));
responseActorRef.tell(response, ActorRef.noSender());
return ok();
} else {
return badRequest(Json.toJson(validationMap));
}
}
My question is: what is ActorRef out and where can I get it in my Controller?
Could you please clarify the logic for sending update to all clients through web sockets?

I'm working on a similar problem, myself, though in Scala, so I'll see if I can assist based on what I've learned so far (I'm having my own problems with getting the message to my actor after the socket opens).
Accepting a WebSocket connection with an actor isn't done with the typical request/response model like making a GET request to the server for a page. Instead, you need to use Play's WebSockets API:
import akka.actor.*;
import play.libs.F.*;
import play.mvc.WebSocket;
public static WebSocket<String> socket() {
return WebSocket.withActor(ResponseActor::props);
}
The Play WebSockets documentation should be able to help you from there better than I can:
https://www.playframework.com/documentation/2.4.x/JavaWebSockets

Related

Spring Boot App not consuming queue messages

I have the Rabbit MQ broker for communicating asynchronously between services. Service A is sending messages to the queue. I checked the queue and the messages from Service A have arrived:
I am trying to create a listener in the Service B in order to consume the messages produced by Service A. I verified like below to check if Service B is connected with RabbitMQ and it seems to be connected successfully.
The problem is that Service B started successfully but it is receiving messages from Rabbit MQ.
Below is the implementation of the listener:
#Slf4j
#Component
public class EventListener {
public static final String QUEUE_NAME = "events";
#RabbitListener(
bindings = {
#QueueBinding(
value = #Queue(QUEUE_NAME),
exchange = #Exchange("exchange")
)
}
)
public void handleTaskPayload(#Payload String payload) {
System.out.println(payload);
}
}
I verified the queue and exchange information in the Rabbit MQ and they are correct.
Everything is working correctly and there is no error thrown in service A or service B which makes this problem much harder to debug.
I tried to retrieve the message from the queue getMessage of RabbitMQ the message is like the below:
{"id":"1",:"name:"Test","created":null}
I will appreciate any help or guidance towards the solution of this problem.
Best Regards,
Rando.
P.S
I created a new test queue like the below and published some messages:
Modified the listener code like below and still wasn't able to trigger listener to listen to the queue events:
#Slf4j
#Component
public class RobotRunEventListener {
public static final String QUEUE_NAME = "test";
#RabbitListener(
bindings = {
#QueueBinding(
value = #Queue(QUEUE_NAME),
key = "test",
exchange = #Exchange("default")
)
}
)
public void handleTaskPayload(#Payload String payload) {
System.out.println(payload);
}
Try this approach:
#RabbitListener(queues = "test")
public void receive(String in, #Headers Map<String, Object> headers) throws IOException {
}
The problem was that the spring boot app that I was working on had a #Conditional(Config.class) that prevented the creation of the bean below:
#Slf4j
#Conditional(Config.class)
#EnableRabbit
public class InternalRabbitBootstrapConfiguration {
#Bean
public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMaxConcurrentConsumers(5);
return factory;
}
...
which resulted in the spring boot app not listening to Rabbit MQ events. The Config.class required a specific profile in order to enable the app to listen to Rabbit MQ events.
public class DexiModeCondition implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
return activeProfiles[0].equalsIgnoreCase(mode);
}
}

Spring+WebSocket+STOMP. Message to specific session (NOT user)

I am trying to set up basic message broker on Spring framework, using a recipe I found here
Author claims it has worked well, but I am unable to receive messages on client, though no visible errors were found.
Goal:
What I am trying to do is basically the same - a client connects to server and requests some async operation. After operation completes the client should receive an event. Important note: client is not authenticated by Spring, but an event from async back-end part of the message broker contains his login, so I assumed it would be enough to store concurrent map of Login-SessionId pairs for sending messages directly to particular session.
Client code:
//app.js
var stompClient = null;
var subscription = '/user/queue/response';
//invoked after I hit "connect" button
function connect() {
//reading from input text form
var agentId = $("#agentId").val();
var socket = new SockJS('localhost:5555/cti');
stompClient = Stomp.over(socket);
stompClient.connect({'Login':agentId}, function (frame) {
setConnected(true);
console.log('Connected to subscription');
stompClient.subscribe(subscription, function (response) {
console.log(response);
});
});
}
//invoked after I hit "send" button
function send() {
var cmd_str = $("#cmd").val();
var cmd = {
'command':cmd_str
};
console.log("sending message...");
stompClient.send("/app/request", {}, JSON.stringify(cmd));
console.log("message sent");
}
Here is my configuration.
//message broker configuration
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
config.enableSimpleBroker("/topic");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/cti")
.setAllowedOrigins("*")
.withSockJS();
}
}
Now, after basic config I should implement an application event handler to provide session-related information on client connect.
//application listener
#Service
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
//this is basically a concurrent map for storing pairs "sessionId - login"
WebAgentSessionRegistry webAgentSessionRegistry;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String agentId = sha.getNativeHeader("Login").get(0);
String sessionId = sha.getSessionId();
/** add new session to registry */
webAgentSessionRegistry.addSession(agentId,sessionId);
//debug: show connected to stdout
webAgentSessionRegistry.show();
}
}
All good so far. After I run my spring webapp in IDE and connected my "clients" from two browser tabs I got this in IDE console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Okay, now let's try to implement message mechanics.
//STOMPController
#Controller
public class STOMPController {
#Autowired
//our registry we have already set up earlier
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
//a helper service which I will post below
MessageSender sender;
#MessageMapping("/request")
public void handleRequestMessage() throws InterruptedException {
Map<String,String> params = new HashMap(1);
params.put("test","test");
//a custom object for event, not really relevant
EventMessage msg = new EventMessage("TEST",params);
//send to user2 (just for the sake of it)
String s_id = webAgentSessionRegistry.getSessionId("user2");
System.out.println("Sending message to user2. Target session: "+s_id);
sender.sendEventToClient(msg,s_id);
System.out.println("Message sent");
}
}
A service to send messages from any part of the application:
//MessageSender
#Service
public class MessageSender implements IMessageSender{
#Autowired
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
SimpMessageSendingOperations messageTemplate;
private String qName = "/queue/response";
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
#Override
public void sendEventToClient(EventMessage event,String sessionId) {
messageTemplate.convertAndSendToUser(sessionId,qName,event,createHeaders(sessionId));
}
}
Now, let's try to test it. I run my IDE, opened Chrome and created 2 tabs form which I connected to server. User1 and User2. Result console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Sending message to user2. Target session: 10g5e10n
Message sent
But, as I mentioned in the beginning - user2 got absolutely nothing, though he is connected and subscribed to "/user/queue/response". No errors either.
A question is, where exactly I am missing the point? I have read many articles on the subject, but to no avail.
SPR-11309 says it's possible and should work. Maybe, id-s aren't actual session id-s?
And well maybe someone knows how to monitor if the message actually has been sent, not dropped by internal Spring mechanics?
SOLUTION UPDATE:
A misconfigured bit:
//WebSocketConfig.java:
....
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
// + parameter "/queue"
config.enableSimpleBroker("/topic","/queue");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
....
I've spent a day debugging internal spring mechanics to find out where exactly it goes wrong:
//AbstractBrokerMessageHandler.java:
....
protected boolean checkDestinationPrefix(String destination) {
if ((destination == null) || CollectionUtils.isEmpty(this.destinationPrefixes)) {
return true;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
//guess what? this.destinationPrefixes contains only "/topic". Surprise, surprise
return true;
}
}
return false;
}
....
Although I have to admit I still think the documentation mentioned that user personal queues aren't to be configured explicitly cause they "already there". Maybe I just got it wrong.
Overall it looks good, but could you change from
config.enableSimpleBroker("/topic");
to
config.enableSimpleBroker("/queue");
... and see if this works? Hope this help.

How to utilize GET response from WSClient in Play Framework 2.5 with Java?

I'm trying to make a get request from Play Framework to another server and save the response body in database. All the answers to similar questions show how to send retrieved data through controller action. But I want to make a request from actor and save the response in DB.
Here is the code of Actor class:
public class TestActor extends UntypedActor {
#Inject
WSClient ws;
#Inject
ExecutionContextExecutor exec;
public static Props props = Props.create(TestActor.class);
public int i = 0;
#Override
public void onReceive(Object msg) throws Exception {
if (msg instanceof String && msg == "doTest") {
Logger.info("I am doing test " + ++i);
CompletionStage<JsonNode> jsonPromise = ws.url("http://localhost:3000").get().thenApply(WSResponse::asJson);
}
}
}
I don't understand how to process that jsonPromise further. Any help would be appreciated.

Adding a new HTTP client to Elasticsearch to support client apps to run against AWS Elasticsearch?

I am trying to add Elasticsearch HTTP access to the Titan ES client using JEST. titan-es only supports ES' local and transport (TCP) mode. But I would like to support communication over ES' HTTP interface. That would allow client libraries like titan-es to use AWS Elasticsearch as an indexing backend which only provides a HTTP(S) interface. See this post for more information.
I am looking for some feedback on the approach I am considering so far:
Create a new class ElasticsearchHttpClient that implements the org.elasticache.client.Client interface. The new class will use the JestClient as it's internal client. This way it will communicate with ES over HTTP. The new class will likely extend ES' AbstractClient to reduce the methods that have to be implemented to: admin(), settings(), execute(), threadPool(), and close().
Add a new enum HTTP_CLIENT to ElasticSearchSetup
Ensure that the connect() method on HTTP_CLIENT returns an instance of Connection which contains proper values for node and client. The client member would be an instance of the new ElasticsearchHttpClient class.
Ensure that ElasticSearchIndex.interfaceConfiguration() method retrieves the correct instance of Connection (containing the new ElasticsearchHttpClient) if the INTERFACE is configured as HTTP_CLIENT. From that point on the rest of the code should continue to work over the new protocol.
Does that sound like it should work? The 1st step is my biggest concern - I am not confident that I can implement all Client methods using the JestClient.
package com.thinkaurelius.titan.diskstorage.es;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestClientFactory;
import io.searchbox.client.JestResult;
import io.searchbox.client.config.HttpClientConfig;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.*;
import org.elasticsearch.client.AdminClient;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.support.AbstractClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import java.io.IOException;
public class ElasticsearchHttpClient extends AbstractClient {
private final JestClient internalClient;
private final ThreadPool pool;
public ElasticsearchHttpClient(String hostname, int port) {
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(new HttpClientConfig
.Builder(String.format("http://%s:%d", hostname, port))
.multiThreaded(true)
.build());
JestClient client = factory.getObject();
this.pool = new ThreadPool("jest");
this.internalClient = client;
}
#Override
public AdminClient admin() {
return null;
}
#Override
public Settings settings() {
return null;
}
#Override
public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder, Client>> ActionFuture<Response> execute(Action<Request, Response, RequestBuilder, Client> action, Request request) {
try {
JestResult response = internalClient.execute(convertRequest(action, request));
return convertResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder, Client>> void execute(Action<Request, Response, RequestBuilder, Client> action, Request request, ActionListener<Response> listener) {
execute(action, request);
}
private <Response extends ActionResponse> ActionFuture<Response> convertResponse(JestResult result) {
// TODO How to convert a JestResult a Elasticsearch ActionResponse/ActionFuture?
return null;
}
private <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder, Client>> io.searchbox.action.Action<JestResult> convertRequest(Action<Request, Response, RequestBuilder, Client> action, Request request) {
// TODO How to convert an Elasticsearch Action<..> and Request to a Jest Action<JestResult>?
return null;
}
#Override
public ThreadPool threadPool() {
return pool;
}
#Override
public void close() throws ElasticsearchException {
pool.shutdownNow();
}
}
[I also asked this on the Titan mailing list and Elasticsearch forum.]
I've posted an answer in the Titan mailing list.
What you'd need to do from a Titan perspective is implement the
IndexProvider interface. My guess is that it isn't feasible to make
Jest look like a full Elasticsearch client.
I think you would use JestHttpClient -- you don't need to implement
the Jest interface. IndexProvider has methods to
create/drop/mutate/query an index, which you should be able to do over
HTTP. Check the Elasticsearch HTTP documentation to see if you can do
all the required methods on IndexProvider with JestHttpClient.
There's already an ElasticSearchIndex implementation of IndexProvider,
which does NODE and TRANSPORT. You're trying to add an HTTP or JEST
option. So you might consider shoehorning your changes into
ElasticSearchIndex, but I'm not sure how well that will work out since
the 2 existing impls are both full Elasticsearch clients. Perhaps
consider creating a separate ElasticSearchHttpIndex implements
IndexProvider if it's cleaner.

Path variables in Spring WebSockets #SendTo mapping

I have, what I think to be, a very simple Spring WebSocket application. However, I'm trying to use path variables for the subscription as well as the message mapping.
I've posted a paraphrased example below. I would expect the #SendTo annotation to return back to the subscribers based on their fleetId. ie, a POST to /fleet/MyFleet/driver/MyDriver should notify subscribers of /fleet/MyFleet, but I'm not seeing this behavior.
It's worth noting that subscribing to literal /fleet/{fleetId} works. Is this intended? Am I missing some piece of configuration? Or is this just not how it works?
I'm not very familiar with WebSockets or this Spring project yet, so thanks in advance.
Controller.java
...
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
...
WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/live");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fleet").withSockJS();
}
}
index.html
var socket = new SockJS('/fleet');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// Doesn't Work
stompClient.subscribe('/topic/fleet/MyFleet', function(greeting) {
// Works
stompClient.subscribe('/topic/fleet/{fleetId}', function(greeting) {
// Do some stuff
});
});
Send Sample
stompClient.send("/live/fleet/MyFleet/driver/MyDriver", {}, JSON.stringify({
// Some simple content
}));
Even though #MessageMapping supports placeholders, they are not exposed / resolved in #SendTo destinations. Currently, there's no way to define dynamic destinations with the #SendTo annotation (see issue SPR-12170). You could use the SimpMessagingTemplate for the time being (that's how it works internally anyway). Here's how you would do it:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
public void simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
simpMessagingTemplate.convertAndSend("/topic/fleet/" + fleetId, new Simple(fleetId, driverId));
}
In your code, the destination '/topic/fleet/{fleetId}' is treated as a literal, that's the reason why subscribing to it works, just because you are sending to the exact same destination.
If you just want to send some initial user specific data, you could return it directly in the subscription:
#SubscribeMapping("/fleet/{fleetId}/driver/{driverId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
Update:
In Spring 4.2, destination variable placeholders are supported it's now possible to do something like:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
you can send a variable inside the path. for example i send "este/es/el/chat/java/" and obtaned in the server as "este:es:el:chat:java:"
client:
stompSession.send("/app/chat/este/es/el/chat/java/*", ...);
server:
#MessageMapping("/chat/**")
#SendToUser("/queue/reply")
public WebsocketData greeting(Message m,HelloMessage message,#Header("simpSessionId") String sessionId) throws Exception {
Map<String, LinkedList<String>> nativeHeaders = (Map<String, LinkedList<String>>) m.getHeaders().get("nativeHeaders");
String value= nativeHeaders.get("destination").getFirst().replaceAll("/app/chat/","").replaceAll("/",":");
Actually I think this is what you might be looking for:
#Autorwired
lateinit var template: SimpMessageTemplate;
#MessageMapping("/class/{id}")
#Throws(Exception::class)
fun onOffer(#DestinationVariable("id") id: String?, #Payload msg: Message) {
println("RECEIVED " + id)
template.convertAndSend("/topic/class/$id", Message("The response"))
}
Hope this helps someone! :)

Categories

Resources