First, let's assume you have a IdUsuario
class that represents (as the name says), the id of some user. This class must be immutable and must implement the equals
and hashCode
methods properly. If you prefer, you can use Long
or String
in place, but I'm going to assume that the id will be something more complicated. I will assume that there is a class IdSession
in the same way that it encapsulates session id
of wowza.
A very simple example of these classes would be this:
public final class IdUsuario {
private final String id;
public IdUsuario(String id) {
if (id == null) throw new IllegalArgumentException();
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
return (obj instanceof IdUsuario) && id.equals(((IdUsuario) obj).id);
}
}
public final class IdSession {
private final String id;
public IdSession(String id) {
if (id == null) throw new IllegalArgumentException();
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
return (obj instanceof IdSession) && id.equals(((IdSession) obj).id);
}
}
However, if you want to enrich these classes with more data that you consider relevant to identify users and sessions, feel free to.
In the same way, I will assume that the session data is stored in a SessaoUsuario
interface that has these methods:
void notificarDesconexao();
void notificarConexao();
So you use the singleton pattern to have a session repository:
public class RepositorioSessoes {
private static final int SESSOES_POR_USUARIO = 3;
private static final RepositorioSessoes REPOSITORIO = new RepositorioSessoes();
public static RepositorioSessoes instance() {
return REPOSITORIO;
}
private final Map<IdUsuario, GrupoSessaoUsuario> grupos;
private RepositorioSessoes() {
sessoes = new ConcurrentHashMap<>();
}
public void conectar(
IdUsuario idUsuario,
IdSession idSession,
BiFunction<IdUsuario, IdSession, SessaoUsuario> criaSessoes)
{
GrupoSessaoUsuario grupo = sessoes.computeIfAbsent(idUsuario, k -> new GrupoSessaoUsuario(k, SESSOES_POR_USUARIO));
grupo.conectar(idSession, criaSessoes);
}
public void desconectarTodos(IdUsuario id) {
GrupoSessaoUsuario grupo = sessoes.get(id);
if (grupo == null) return;
grupo.limpar();
sessoes.remove(id);
}
private static class GrupoSessaoUsuario {
private final IdUsuario idUsuario;
private final int limite;
private final Map<IdSession, SessaoUsuario> sessoes;
public GrupoSessaoUsuario(IdUsuario idUsuario, int limite) {
this.idUsuario = idUsuario;
this.sessoes = new LinkedHashMap<>(limite);
this.limite = limite;
}
public synchronized void conectar(
IdSession idSession,
BiFunction<IdUsuario, IdSession, SessaoUsuario> criaSessoes)
{
SessaoUsuario novaSessao = null;
if (sessoes.containsKey(idSession)) {
novaSessao = sessoes.remove(idSession);
} else if (sessoes.size() >= limite) {
Iterator<SessaoUsuario> it = sessoes.values().iterator();
it.next().notificarDesconexao();
it.remove();
}
if (novaSessao != null) novaSessao = criaSessoes.apply(idUsuario, idSession);
sessoes.put(idSession, novaSessao);
novaSessao.notificarConexao();
}
public synchronized void limpar() {
for (SessaoUsuario s : sessoes.values()) {
s.notificarDesconexao();
}
}
}
}
Whenever the user connects, you call the conectar(IdUsuario, IdSession, BiFunction<IdUsuario, IdSession, SessaoUsuario>)
method. When you want to give kill , call desconectarTodos(IdUsuario)
. The approach used here is not to create a thread to control this, but to create an object to control it.
The conectar
method is a bit tricky to use because of this BiFunction
, but it's not very difficult to do it. Let's suppose you have somewhere a function to create a SessaoUsuario
like this:
public SessaoUsuario criarSessao(IdUsuario idUsuario, IdSession idSession) {
...
}
So, you would call it like this:
IdUsuario idUsuario = ...;
IdSession idSession = ...;
RepositorioSessoes.instance().conectar(idUsuario, idSession, this::criarSessao);
Or, you can use a constructor of SessaoUsuario
:
public SessaoUsuario(IdUsuario idUsuario, IdSession idSession) {
...
}
So, you would call it like this:
IdUsuario idUsuario = ...;
IdSession idSession = ...;
RepositorioSessoes.instance().conectar(idUsuario, idSession, SessaoUsuario::new);
The class RepositorioSessoes
is in charge to call when pertinent and necessary, the notificarConexao
and notificarDesconexao
methods.
The inner class GrupoSessaoUsuario
(which is not public) manages all three user sessions. When it connects to an existing session, it goes to the end of the queue (in this case, this becomes the newest one). If there are already three sessions, it will remove the oldest.
The methods of the GrupoSessaoUsuario
class are synchronized to ensure that two concurrent threads on different threads do not end up cluttering the GrupoSessaoUsuario
internal state. This synchronization occurs on the GrupoSessaoUsuario
object, and since each user must have one and only one instance of this object, then different threads from different users will not compete for this object, only threads from the same user will do so. The only place this object is created is in the method computeIfAbsent
of ConcurrentHashMap
" which has the guarantee of being atomic, and therefore there will be only one instance of this for each IdUsuario
and therefore one for each user.
This class should work both in case you never reuse session ids and in case you always reuse them.
The above implementation can be simplified somewhat if your IdSession
is implemented in a way that you can reach IdUsuario
directly (for example, if IdSession
has a field with IdUsuario
).