package de.rwth.idsg.steve.ocpp.ws;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import de.rwth.idsg.steve.SteveException;
import de.rwth.idsg.steve.ocpp.ws.custom.WsSessionSelectStrategy;
import de.rwth.idsg.steve.ocpp.ws.data.SessionContext;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* @author Sevket Goekay <goekay@dbis.rwth-aachen.de>
* @since 17.03.2015
*/
@Slf4j
public class SessionContextStoreImpl implements SessionContextStore {
/**
* Key (String) = chargeBoxId
* Value (Deque<SessionContext>) = WebSocket session contexts
*/
private final ConcurrentHashMap<String, Deque<SessionContext>> lookupTable = new ConcurrentHashMap<>();
private final WsSessionSelectStrategy wsSessionSelectStrategy;
public SessionContextStoreImpl(WsSessionSelectStrategy wsSessionSelectStrategy) {
this.wsSessionSelectStrategy = wsSessionSelectStrategy;
}
@Override
public void add(String chargeBoxId, WebSocketSession session, ScheduledFuture pingSchedule) {
SessionContext context = new SessionContext(session, pingSchedule, DateTime.now());
Deque<SessionContext> endpointDeque = lookupTable.computeIfAbsent(chargeBoxId, str -> new ArrayDeque<>());
endpointDeque.addLast(context); // Adding at the end
log.debug("A new SessionContext is stored for chargeBoxId '{}'. Store size: {}",
chargeBoxId, endpointDeque.size());
}
@Override
public void remove(String chargeBoxId, WebSocketSession session) {
Deque<SessionContext> endpointDeque = lookupTable.get(chargeBoxId);
if (endpointDeque == null) {
log.debug("No session context to remove for chargeBoxId '{}'", chargeBoxId);
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
//
SessionContext toRemove = null;
for (SessionContext context : endpointDeque) {
if (context.getSession().getId().equals(session.getId())) {
toRemove = context;
break;
}
}
if (toRemove != null) {
// 1. Cancel the ping task
toRemove.getPingSchedule().cancel(true);
// 2. Delete from collection
if (endpointDeque.remove(toRemove)) {
log.debug("A SessionContext is removed for chargeBoxId '{}'. Store size: {}",
chargeBoxId, endpointDeque.size());
}
// 3. Delete empty collection from lookup table in order to correctly calculate
// the number of connected chargeboxes with getNumberOfChargeBoxes()
if (endpointDeque.size() == 0) {
lookupTable.remove(chargeBoxId);
}
}
}
@Override
public int getSize(String chargeBoxId) {
Deque<SessionContext> endpointDeque = lookupTable.get(chargeBoxId);
if (endpointDeque == null) {
return 0;
} else {
return endpointDeque.size();
}
}
@Override
public List<String> getChargeBoxIdList() {
return Collections.list(lookupTable.keys());
}
@Override
public Map<String, Deque<SessionContext>> getACopy() {
return ImmutableMap.copyOf(lookupTable);
}
@Override
public int getNumberOfChargeBoxes() {
return lookupTable.size();
}
@Override
public WebSocketSession getSession(String chargeBoxId) {
if (Strings.isNullOrEmpty(chargeBoxId)) {
throw new SteveException("Invalid chargeBoxId (null or empty)");
}
try {
Deque<SessionContext> endpointDeque = lookupTable.get(chargeBoxId);
if (endpointDeque == null) {
throw new NoSuchElementException();
}
return wsSessionSelectStrategy.getSession(endpointDeque);
} catch (NoSuchElementException e) {
throw new SteveException("No session context for chargeBoxId '%s'", chargeBoxId, e);
}
}
}