/*
* Copyright 2012 Research Studios Austria Forschungsges.m.b.H.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package won.owner.web.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import won.owner.model.User;
import won.owner.model.UserNeed;
import won.owner.repository.UserNeedRepository;
import won.owner.repository.UserRepository;
import won.owner.service.impl.OwnerApplicationService;
import won.owner.web.WonOwnerMailSender;
import won.protocol.message.WonMessage;
import won.protocol.message.WonMessageDecoder;
import won.protocol.message.WonMessageEncoder;
import won.protocol.message.WonMessageType;
import won.protocol.message.processor.WonMessageProcessor;
import won.protocol.util.WonRdfUtils;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* User: syim
* Date: 06.08.14
*/
public class WonWebSocketHandler
extends TextWebSocketHandler
implements WonMessageProcessor, InitializingBean
{
private final Logger logger = LoggerFactory.getLogger(getClass());
//if we're receiving a partial message, a StringBuilder will be in the session's attributes map under this key
private static final String SESSION_ATTRIBUTE_PARTIAL_MESSAGE = "partialMessage";
private OwnerApplicationService ownerApplicationService;
@Autowired
private WebSocketSessionService webSocketSessionService;
@Autowired
private UserRepository userRepository;
@Autowired
private UserNeedRepository userNeedRepository;
@Autowired
SessionRepository sessionRepository;
@Autowired
private WonOwnerMailSender emailSender;
@Override
public void afterPropertiesSet() throws Exception {
this.ownerApplicationService.setMessageProcessorDelegate(this);
}
@Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
//remember which user or (if not logged in) which needUri the session is bound to
User user = getUserForSession(session);
if (user != null) {
logger.debug("connection established, binding session to user {}", user.getId());
this.webSocketSessionService.addMapping(user, session);
} else {
logger.debug("connection established, but no user found in session to bind to");
}
}
@Override
public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
User user = getUserForSession(session);
if (user != null) {
logger.debug("session closed, removing session bindings to user {}", user.getId());
this.webSocketSessionService.removeMapping(user, session);
for (UserNeed userNeed : user.getUserNeeds()){
logger.debug("removing session bindings to need {}", userNeed.getUri());
this.webSocketSessionService.removeMapping(userNeed.getUri(), session);
}
} else {
logger.debug("connection closed, but no user found in session, no bindings removed");
}
}
/*User user = getCurrentUser();
logger.info("New Need:" + needPojo.getTextDescription() + "/" + needPojo.getCreationDate() + "/" +
needPojo.getLongitude() + "/" + needPojo.getLatitude() + "/" + (needPojo.getState() == NeedState.ACTIVE));
//TODO: using fixed Facets - change this
needPojo.setFacetTypes(new String[]{
FacetType.OwnerFacet.getURI().toString()});
NeedPojo createdNeedPojo = resolve(needPojo);
Need need = needRepository.findOne(createdNeedPojo.getNeedId());
user.getNeeds().add(need);
wonUserDetailService.save(user);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(need.getNeedURI());
return new ResponseEntity<NeedPojo>(createdNeedPojo, headers, HttpStatus.CREATED); */
@Override
@Transactional(propagation = Propagation.SUPPORTS, isolation = Isolation.READ_COMMITTED)
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException
{
logger.debug("OA Server - WebSocket message received: {}", message.getPayload());
updateSession(session);
if (!message.isLast()){
//we have an intermediate part of the current message.
session.getAttributes()
.putIfAbsent(SESSION_ATTRIBUTE_PARTIAL_MESSAGE, new StringBuilder());
}
//now check if we have the partial message string builder in the session.
//if we do, we're processing a partial message, and we have to append the current message payload
StringBuilder sb = (StringBuilder) session.getAttributes().get(SESSION_ATTRIBUTE_PARTIAL_MESSAGE);
String completePayload = null; //will hold the final message
if (sb == null) {
//No string builder found in the session - we're not processing a partial message.
//The complete payload is in the current message. Get it and continue.
completePayload = message.getPayload();
} else {
//the string builder is there - we're processing a partial message. append the current piece
sb.append(message.getPayload());
if (message.isLast()){
//we've received the last part. pass it on to the next processing steps.
completePayload = sb.toString();
//also, we do not need the string builder in the session any longer. remove it:
session.getAttributes().remove(SESSION_ATTRIBUTE_PARTIAL_MESSAGE);
} else {
//This is not the last part of the message.
//We have stored it along with all previous parts. Abort processing this message and wait for the
// next part
return;
}
}
WonMessage wonMessage = WonMessageDecoder.decodeFromJsonLd(completePayload);
//remember which user or (if not logged in) which needUri the session is bound to
User user = getUserForSession(session);
if (user != null) {
logger.debug("binding session to user {}", user.getId());
this.webSocketSessionService.addMapping(user, session);
}
//anyway, we have to bind the URI to the session, otherwise we can't handle incoming server->client messages
URI needUri = wonMessage.getSenderNeedURI();
logger.debug("binding session to need URI {}", needUri);
this.webSocketSessionService.addMapping(needUri, session);
ownerApplicationService.sendWonMessage(wonMessage);
}
/* update the session last accessed time, - spring-session was added to synchronize
// http sessions with websocket session
// see: http://spring.io/blog/2014/09/16/preview-spring-security-websocket-support-sessions
// Currently used here Sping's MapSession implementation of Spring has default timeout of 30 minutes
*/
private void updateSession(final WebSocketSession session) {
String sessionId = (String) session.getAttributes().get(WonHandshakeInterceptor.SESSION_ATTR);
if (sessionId != null) {
Session activeSession = sessionRepository.getSession(sessionId);
if (session != null) {
sessionRepository.save(activeSession);
}
}
}
/**
* We want to keep the buffer in the underlying server small (8k per websocket), but still
* be able to receive large messages. Hence, we have to be able to handle partial messages here.
*/
@Override
public boolean supportsPartialMessages() {
return true;
}
@Override
@Transactional(propagation = Propagation.SUPPORTS, isolation = Isolation.READ_COMMITTED)
public WonMessage process(final WonMessage wonMessage) {
String wonMessageJsonLdString = WonMessageEncoder.encodeAsJsonLd(wonMessage);
WebSocketMessage<String> webSocketMessage = new TextMessage(wonMessageJsonLdString);
URI needUri = wonMessage.getReceiverNeedURI();
User user = getUserForWonMessage(wonMessage);
Set<WebSocketSession> webSocketSessions = findWebSocketSessionsForWonMessage(wonMessage, needUri, user);
//check if we can deliver the message. If not, send email.
if (webSocketSessions.size() == 0) {
logger.info("cannot deliver message of type {} for need {}, receiver {}: no websocket session found",
new Object[]{wonMessage.getMessageType(),
wonMessage.getReceiverNeedURI(),
wonMessage.getReceiverURI()});
// send per email notifications if it applies:
notifyPerEmail(user, needUri, wonMessage);
return wonMessage;
}
for (WebSocketSession session : webSocketSessions) {
sendMessageForSession(wonMessage, webSocketMessage, session, needUri, user);
}
return wonMessage;
}
private void notifyPerEmail(final User user, final URI needUri, final WonMessage wonMessage) {
if (user == null) {
return;
}
UserNeed userNeed = getNeedOfUser(user, needUri);
if (userNeed == null) {
return;
}
String textMsg = WonRdfUtils.MessageUtils.getTextMessage(wonMessage);
try {
switch (wonMessage.getMessageType()) {
case OPEN:
if (userNeed.isConversations()) {
emailSender.sendConversationNotificationHtmlMessage(
user.getEmail(), needUri.toString(), wonMessage.getSenderNeedURI().toString(), wonMessage.getReceiverURI().toString(), textMsg);
}
return;
case CONNECTION_MESSAGE:
if (userNeed.isConversations()) {
emailSender.sendConversationNotificationHtmlMessage(
user.getEmail(), needUri.toString(), wonMessage.getSenderNeedURI().toString(), wonMessage.getReceiverURI().toString(), textMsg);
}
return;
case CONNECT:
if (userNeed.isRequests()) {
emailSender.sendConnectNotificationHtmlMessage(
user.getEmail(), needUri.toString(), wonMessage.getSenderNeedURI().toString(), wonMessage.getReceiverURI().toString(), textMsg);
}
return;
case HINT_MESSAGE:
if (userNeed.isMatches()) {
String remoteNeedUri = WonRdfUtils.MessageUtils.toMatch(wonMessage).getToNeed().toString();
emailSender.sendHintNotificationMessageHtml(user.getEmail(), needUri.toString(), remoteNeedUri,wonMessage
.getReceiverURI().toString());
}
return;
case CLOSE:
//a close message is only received for an established connection. If the user
//wants to be notified of requests, they will get closes as well
if (userNeed.isRequests()) {
emailSender.sendCloseNotificationHtmlMessage(
user.getEmail(), needUri.toString(), wonMessage.getSenderNeedURI().toString(), wonMessage
.getReceiverURI().toString(), textMsg);
}
return;
default:
return;
}
} catch (MailException ex) { // org.springframework.mail.MailException
logger.error("Email could not be sent", ex);
}
}
private Set<WebSocketSession> findWebSocketSessionsForWonMessage(final WonMessage wonMessage, URI needUri,
User user) {
assert wonMessage != null : "wonMessage must not be null";
assert needUri != null : "needUri must not be null";
Set<WebSocketSession> webSocketSessions =
webSocketSessionService.getWebSocketSessions(needUri);
if (webSocketSessions == null) webSocketSessions = new HashSet();
logger.debug("found {} sessions for need uri {}, now removing closed sessions", webSocketSessions.size(), needUri);
removeClosedSessions(webSocketSessions, needUri);
if (user != null){
Set<WebSocketSession> userSessions = webSocketSessionService.getWebSocketSessions(user);
if (userSessions == null) userSessions = new HashSet();
logger.debug("found {} sessions for user {}, now removing closed sessions", userSessions.size(), user.getId());
removeClosedSessions(userSessions, user);
webSocketSessions.addAll(userSessions);
}
return webSocketSessions;
}
private void removeClosedSessions(final Set<WebSocketSession> webSocketSessions, final URI needUri) {
for (Iterator<WebSocketSession> it = webSocketSessions.iterator(); it.hasNext(); ){
WebSocketSession session = it.next();
if (!session.isOpen()) {
logger.debug("removing closed websocket session {} of need {}", session.getId(), needUri);
webSocketSessionService.removeMapping(needUri, session);
it.remove();
}
}
}
private void removeClosedSessions(final Set<WebSocketSession> webSocketSessions, final User user) {
for (Iterator<WebSocketSession> it = webSocketSessions.iterator(); it.hasNext(); ){
WebSocketSession session = it.next();
if (!session.isOpen()) {
logger.debug("removing closed websocket session {} of user {}", session.getId(), user.getId());
webSocketSessionService.removeMapping(user, session);
it.remove();
}
}
}
private User getUserForWonMessage(final WonMessage wonMessage) {
URI needUri = wonMessage.getReceiverNeedURI();
return userRepository.findByNeedUri(needUri);
}
private UserNeed getNeedOfUser(final User user, final URI needUri) {
for (UserNeed userNeed : user.getUserNeeds()) {
if (userNeed.getUri().equals(needUri)) {
return userNeed;
}
}
return null;
}
private synchronized void sendMessageForSession(final WonMessage wonMessage, final WebSocketMessage<String>
webSocketMessage,
final WebSocketSession session, URI needUri, User user) {
if (!session.isOpen()){
logger.debug("session {} is closed, can't send message", session.getId());
return;
}
if (wonMessage.getMessageType() == WonMessageType.SUCCESS_RESPONSE
&& WonMessageType.CREATE_NEED ==wonMessage.getIsResponseToMessageType()){
if (session.getPrincipal() != null) {
saveNeedUriWithUser(wonMessage, session);
} else {
logger.warn("could not associate need {} with currently logged in user: no principal found in session");
}
}
try {
logger.debug("OA Server - sending WebSocket message: {}", webSocketMessage);
session.sendMessage(webSocketMessage);
} catch (Exception e) {
logger.warn(MessageFormat.format("caught exception while trying to send on session {1} for needUri {2}, " +
"user {3}", session.getId(), needUri, user), e);
if (user != null){
webSocketSessionService.removeMapping(user, session);
}
if (needUri != null) {
webSocketSessionService.removeMapping(needUri, session);
}
}
}
private void saveNeedUriWithUser(final WonMessage wonMessage, final WebSocketSession session) {
User user = getUserForSession(session);
URI needURI = wonMessage.getReceiverNeedURI();
UserNeed userNeed = new UserNeed(needURI);
userNeedRepository.save(userNeed);
user.addNeedUri(userNeed);
userRepository.save(user);
}
private User getUserForSession(final WebSocketSession session) {
if (session == null) {
return null;
}
if (session.getPrincipal() == null){
return null;
}
String username = session.getPrincipal().getName();
return userRepository.findByUsername(username);
}
public void setOwnerApplicationService(final OwnerApplicationService ownerApplicationService) {
this.ownerApplicationService = ownerApplicationService;
}
}