/*
*
* 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;
}
}