/* * * Copyright (C) 2012-2014 R T Huitema. All Rights Reserved. * Web: www.42.co.nz * Email: robert@42.co.nz * Author: R T Huitema * * This file is part of the signalk-server-java project * * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * * 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 nz.co.fortytwo.signalk.server; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; /** * Track and manage the sessionId's and corresponding webSocket identifiers and subscriptions for a consumer * * @author robert * */ public class SubscriptionManager { private static Logger logger = LogManager.getLogger(SubscriptionManager.class); //hold sessionid <> wsSessionId BiMap<String, String> wsSessionMap = HashBiMap.create(); //map wsSession to output type Map<String, String> outPutMap = new HashMap<String, String>(); //wsSession> localIp#remoteIp Map<String, String> ipMap = new HashMap<String, String>(); //wsSessionId>Subscription ConcurrentLinkedQueue<Subscription> subscriptions = new ConcurrentLinkedQueue<Subscription>(); ConcurrentLinkedQueue<String> heartbeats = new ConcurrentLinkedQueue<String>(); /** * Add a new subscription. * @param sub * @throws Exception */ public void addSubscription(Subscription sub) throws Exception{ if(!subscriptions.contains(sub)){ if(logger.isDebugEnabled())logger.debug("Adding sub "+sub); subscriptions.add(sub); //create a new route if we have too if(sub.isActive() && !hasExistingRoute(sub)){ RouteManager routeManager = RouteManagerFactory.getInstance(); SignalkRouteFactory.configureSubscribeTimer(routeManager, sub); if(logger.isDebugEnabled())logger.debug("Started route for sub"+sub); heartbeats.remove(sub.getWsSession()); } if(logger.isDebugEnabled())logger.debug("Subs size ="+subscriptions.size()); } } /** * True if another subscription has the same route and is active * @param sub * @return */ private boolean hasExistingRoute(Subscription sub) { for(Subscription s: getSubscriptions(sub.getWsSession())){ if(sub.equals(s))continue; if(sub.isSameRoute(s)&&s.isActive()){ sub.setRouteId(s.getRouteId()); return true; } }; return false; } /** * Remove a subscription * * @param sub * @throws Exception */ public void removeSubscription(Subscription sub) throws Exception{ subscriptions.remove(sub); if(sub.isActive()&& !hasExistingRoute(sub)){ RouteManager routeManager = RouteManagerFactory.getInstance(); SignalkRouteFactory.removeSubscribeTimer(routeManager, sub); } //if we have no subs, then we should put a sub for empty updates as heartbeat if(getSubscriptions(sub.getWsSession()).size()==0){ heartbeats.add(sub.getWsSession()); } } public ConcurrentLinkedQueue<Subscription> getSubscriptions(String wsSession){ ConcurrentLinkedQueue<Subscription> subs = new ConcurrentLinkedQueue<Subscription>(); for (Subscription s: subscriptions){ if(s.getWsSession().equals(wsSession)){ subs.add(s); } } return subs; } /** * Returns the wsSessionId for the sessionId if it exists * Returns the sessionId if not. This allows for subscriptions to occur before wsSocket starts * @param sessionId * @return */ public String getWsSession(String sessionId){ if(!wsSessionMap.containsKey(sessionId))return sessionId; return wsSessionMap.get(sessionId); } public String getSessionId(String wsSession){ return wsSessionMap.inverse().get(wsSession); } /** * Inserts the sessionId, wsSession pair. * Swaps the wsSessionId for any any inactive sessions that have been entered with sessionId, sessionId * If this is a new connection with no subs then nothing will be tx'd * @param sessionId * @param wsSession * @param ipAddress * @param string * @throws Exception */ public void add(String sessionId, String wsSession, String outputType, String localIpAddress, String remoteIpAddress) throws Exception{ if(StringUtils.isBlank(wsSession) || StringUtils.isBlank(sessionId))return; wsSessionMap.put(sessionId, wsSession); outPutMap.put(wsSession, outputType); ipMap.put(wsSession, localIpAddress+"#"+remoteIpAddress); logger.debug("Adding "+sessionId+"/"+wsSession+", outputType="+outputType+", localAddress:"+localIpAddress+", remoteAddress:"+remoteIpAddress); //now update any subscriptions for sessionId ConcurrentLinkedQueue<Subscription> subs = getSubscriptions(sessionId); for (Subscription s: subs){ if(s.getWsSession().equals(sessionId)){ subscriptions.remove(s); s.setWsSession(wsSession); subscriptions.add(s); } s.setActive(true); if(!hasExistingRoute(s)){ RouteManager routeManager = RouteManagerFactory.getInstance(); SignalkRouteFactory.configureSubscribeTimer(routeManager, s); } } //if we have no subs, then we should put a sub for empty updates as heartbeat if(getSubscriptions(wsSession).size()==0){ heartbeats.add(wsSession); } } public void removeAllSessions() throws Exception{ wsSessionMap.clear(); outPutMap.clear(); ipMap.clear(); //remove all subscriptions RouteManager routeManager = RouteManagerFactory.getInstance(); SignalkRouteFactory.removeSubscribeTimers(routeManager,subscriptions ); subscriptions.clear(); heartbeats.clear(); } public void removeSessionId(String sessionId) throws Exception{ String wsSession = wsSessionMap.get(sessionId); wsSessionMap.remove(sessionId); outPutMap.remove(wsSession); ipMap.remove(wsSession); //remove all subscriptions RouteManager routeManager = RouteManagerFactory.getInstance(); ConcurrentLinkedQueue<Subscription> subs = getSubscriptions(wsSession); SignalkRouteFactory.removeSubscribeTimers(routeManager,subs ); subscriptions.removeAll(subs); subscriptions.removeAll(getSubscriptions(sessionId)); heartbeats.remove(wsSession); } public void removeWsSession(String wsSession) throws Exception{ wsSessionMap.inverse().remove(wsSession); outPutMap.remove(wsSession); ipMap.remove(wsSession); //remove all subscriptions RouteManager routeManager = RouteManagerFactory.getInstance(); ConcurrentLinkedQueue<Subscription> subs = getSubscriptions(wsSession); SignalkRouteFactory.removeSubscribeTimers(routeManager, subs); subscriptions.removeAll(subs); heartbeats.remove(wsSession); } /** * Returns a Set of all the current sessionIds. * * @return */ public Set<String> getSessionKeys() { return wsSessionMap.keySet(); } public String getOutputType(String wsSession){ return outPutMap.get(wsSession); } /** * Return the ipAddress of the client on this websocket session * @param wsSession * @return */ public String getRemoteIpAddress(String wsSession){ String ips = ipMap.get(wsSession); if(StringUtils.isBlank(ips)) return null; return ips.split("#")[1]; } public String getLocalIpAddress(String wsSession){ String ips = ipMap.get(wsSession); if(StringUtils.isBlank(ips)) return null; return ips.split("#")[0]; } /** * Gets a Set of all the current wsSessions * @return */ public Set<String> getWsSessionKeys() { return wsSessionMap.inverse().keySet(); } public boolean isValid(String sessionId) { if(wsSessionMap.containsKey(sessionId))return true; return false; } public ConcurrentLinkedQueue<String> getHeartbeats() { return heartbeats; } }