package controllers.websockets;
import static play.libs.F.Matcher.ClassOf;
import static play.libs.F.Matcher.Equals;
import static play.mvc.Http.WebSocketEvent.SocketClosed;
import static play.mvc.Http.WebSocketEvent.TextFrame;
import java.util.List;
import java.util.Timer;
import oauth2.CheckUserAuthentication;
import oauth2.OAuth2Constants;
import play.Logger;
import play.libs.F.E3;
import play.libs.F.EventStream;
import play.libs.F.Promise;
import play.mvc.WebSocketController;
import play.mvc.Http.WebSocketClose;
import play.mvc.Http.WebSocketEvent;
import utils.GsonFactory;
import DTO.UserDTO;
import DTO.UserLocationDTO;
import assemblers.UserLocationAssembler;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* The WebSocket Controller class which handles sending and receiving
* location update events to/from clients.
*
* @author Alex Jarvis axj7@aber.ac.uk
*/
public class LocationsSocket extends WebSocketController {
public static void connect() {
Logger.info("WebSocket opened");
// If not a valid token then disconnect the stream
CheckUserAuthentication userAuth = new CheckUserAuthentication();
UserDTO currentUserDTO = null;
if (!userAuth.validToken(params.get(OAuth2Constants.PARAM_OAUTH_TOKEN))) {
disconnect();
} else {
currentUserDTO = userAuth.getAuthorisedUserDTO();
}
// Socket connected, get the location stream and events for this user.
LocationStreamManager locationStreamManager = LocationStreamManager.getInstance();
EventStream<LocationEvent> locationStream = locationStreamManager.getLocationStreamForUserWithId(currentUserDTO.id);
// Create a new heartbeat stream for this socket
EventStream<HeartbeatEvent> heartbeatStream = new EventStream<HeartbeatEvent>();
// Schedule a heartbeat event every 10 seconds
HeartbeatTask heartbeatMonitor = new HeartbeatTask(heartbeatStream);
Timer timer = new Timer();
timer.scheduleAtFixedRate(heartbeatMonitor, 10000, 10000);
// Loop while the socket is open
while(inbound.isOpen()) {
E3<WebSocketEvent, HeartbeatEvent, LocationEvent> e = await(Promise.waitEither(
inbound.nextEvent(),
heartbeatStream.nextEvent(),
locationStream.nextEvent()
));
// Case: The socket has been closed
for(WebSocketClose closed : SocketClosed.match(e._1)) {
Logger.info("WebSocket closed");
locationStreamManager.closeStreamForUserWithId(currentUserDTO.id);
disconnect();
}
// Case: HeartbeatEvent received (from client)
for(String text: TextFrame.and(Equals(MessageWrapper.wrap("~h~PONG"))).match(e._1)) {
Logger.debug("Heartbeat received");
heartbeatMonitor.setResponse(true);
}
// Case: HeartbeatEvent.Pulse received (from heartbeatMonitor)
for (HeartbeatEvent event : ClassOf(HeartbeatEvent.Pulse.class).match(e._2)) {
Logger.debug("HeartbeatEvent Pulse from timer");
heartbeatMonitor.setResponse(false);
outbound.send(MessageWrapper.wrap("~h~PING"));
Logger.debug("Heartbeat sent");
}
// Case: HeartbeatEvent.Dead received (from heartbeatMonitor)
for (HeartbeatEvent event : ClassOf(HeartbeatEvent.Dead.class).match(e._2)) {
Logger.debug("HeartbeatEvent Dead from timer");
disconnect();
}
// Case: Update location message sent from client (only 1 type of message can be sent - location update).
// This message is sent as a Json Array and so we can specify that it will always start with '['.
for(String message: TextFrame.match(e._1)) {
String unwrappedMessage = MessageWrapper.unwrap(message);
if (isJson(unwrappedMessage)) {
String jsonString = removeJsonHeader(unwrappedMessage);
Logger.debug("WebSocket message received:" + jsonString);
JsonArray jsonArray = stringToJsonArray(jsonString);
if (jsonArray != null && jsonArray.isJsonArray()) {
// Obtain DTOs from the JsonArray
List<UserLocationDTO> userLocationDTOs = UserLocationAssembler.userLocationDTOsWithJsonArray(jsonArray);
// Persist locations with the DTOs
List<UserLocationDTO> createdUserLocationDTOs = UserLocationAssembler.createUserLocations(userLocationDTOs, currentUserDTO);
LocationStreamHelper.publishNewUserLocations(createdUserLocationDTOs, currentUserDTO);
}
}
}
// Case: Another user has updated their locations and it is on this users stream.
for(LocationEvent.OtherUserUpdated otherUserUpdated : ClassOf(LocationEvent.OtherUserUpdated.class).match(e._3)) {
String jsonString = objectToJsonString(otherUserUpdated.locations);
Logger.debug("WebSocket sending:\n" + jsonString);
outbound.send(MessageWrapper.wrap("~j~" + jsonString));
}
} // end while socket open
}
private static boolean isJson(String message) {
return MessageWrapper.unwrap(message).startsWith("~j~");
}
private static String removeJsonHeader(String message) {
return message.substring(3);
}
/**
* Returns a JSON string from an Object. This is usually handled by the RenderJSON method
* of play.mvc.Controller
*
* There is a convenience method for WebSocketControllers (outbound.sendJson) - but this
* method does not let you control the behaviour of the created Gson object (as I need to
* specify the date format) and it cannot be overidden in this class because its an internal
* class inside play.mvc.Http.
*
* @param object
* @return
* @see play.mvc.Controller
* @see play.mvc.Http
*/
private static String objectToJsonString(Object object) {
return GsonFactory.gsonBuilder().create().toJson(object);
}
/**
* Returns a JsonArray from a String. Usually this is done automatically by GsonArrayBinder
* for a normal play.mvc.Controller class.
*
* @param jsonArray
* @return
* @see play.mvc.Controller
*/
private static JsonArray stringToJsonArray(String jsonArray) {
return (JsonArray) new JsonParser().parse(jsonArray);
}
}