package de.rwth.idsg.bikeman.ixsi.store; import com.google.common.base.Optional; import de.rwth.idsg.bikeman.ixsi.store.WebSocketSessionStore; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.socket.WebSocketSession; import java.util.ArrayDeque; import java.util.Deque; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; /** * We want to support multiple connections from a client system. * * 1) During a request/response communication, the response is sent using the same WebSocketSession, * since we reference it in a CommunicationContext and pass it on. At the end of the process chain * the Producer sends the response using the referenced WebSocketSession. * * 2) For the push messages we need a mechanism to select one WebSocketSession from the set. * This is done in a round robin fashion. See getNext(). * * @author Sevket Goekay <goekay@dbis.rwth-aachen.de> * @since 05.11.2014 */ @Slf4j @Service public class WebSocketSessionStoreImpl implements WebSocketSessionStore { /** * Key (String) = ID of the client system * Value (Deque<WebSocketSession>) = WebSocket connections of the client system */ @Getter private final ConcurrentHashMap<String, Deque<WebSocketSession>> lookupTable = new ConcurrentHashMap<>(); @Override public synchronized void add(String systemID, WebSocketSession session) { Deque<WebSocketSession> sessionSet = lookupTable.get(systemID); if (sessionSet == null) { sessionSet = new ArrayDeque<>(); sessionSet.add(session); lookupTable.put(systemID, sessionSet); } else { sessionSet.addLast(session); // Adding at the end } log.debug("A new WebSocketSession with id '{}' is stored for system '{}' (size: {})", session.getId(), systemID, sessionSet.size()); } @Override public synchronized void remove(String systemID, WebSocketSession session) { Deque<WebSocketSession> sessionSet = lookupTable.get(systemID); if (sessionSet == null) { return; } // Prevent java.util.ConcurrentModificationException: null // Reason: Cannot modify the set (remove the item) we are iterating // Solution: Iterate the set, find the item, remove the item after the for-loop // WebSocketSession toRemove = null; for (WebSocketSession wss : sessionSet) { if (wss.getId().equals(session.getId())) { toRemove = wss; break; } } if (toRemove != null && sessionSet.remove(toRemove)) { log.debug("The WebSocketSession with id '{}' is removed for system '{}' (size: {})", session.getId(), systemID, sessionSet.size()); } else { log.error("Failed to remove the WebSocketSession with id '{}' for system '{}'", session.getId(), systemID); } } @Override public synchronized Optional<WebSocketSession> get(String systemID, String sessionID) { Deque<WebSocketSession> sessionSet = lookupTable.get(systemID); if (sessionSet != null) { for (WebSocketSession wss : sessionSet) { if (wss.getId().equals(sessionID)) { return Optional.of(wss); } } } return Optional.absent(); } /** * sessionSet.removeFirst() will throw NoSuchElementException, if the sessionSet is empty. * * But the sessionSet itself might be null, if there is no connection established and therefore, * the sessionSet is not initialized, yet. In this case we throw the same exception, * since the ProducerImpl decides on the context based on the exception type. */ @Override public synchronized WebSocketSession getNext(String systemID) { Deque<WebSocketSession> sessionSet = lookupTable.get(systemID); if (sessionSet == null) { throw new NoSuchElementException(); } // Get the first item, and add at the end WebSocketSession s = sessionSet.removeFirst(); sessionSet.addLast(s); return s; } @Override public int size(String systemID) { Deque<WebSocketSession> sessionSet = lookupTable.get(systemID); if (sessionSet == null) { return 0; } else { return sessionSet.size(); } } @Override public void clear() { lookupTable.clear(); log.debug("Cleared the WebSocketSession store"); } @Override public String toString() { return lookupTable.toString(); } }