package restx.security;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import java.util.concurrent.ConcurrentMap;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Date: 17/11/13
* Time: 16:23
*/
public class Sessions {
public static final class SessionData implements Comparable<SessionData> {
private final String key;
private final long firstAccess;
private final long lastAccess;
private final long lastAccessNano;
private final int count;
private final ImmutableMap<String, String> metadata;
private SessionData(String key, long firstAccess, long lastAccess, long lastAccessNano, int count, ImmutableMap<String, String> metadata) {
this.key = checkNotNull(key);
this.firstAccess = firstAccess;
this.lastAccess = lastAccess;
this.lastAccessNano = lastAccessNano;
this.count = count;
this.metadata = checkNotNull(metadata);
}
public String getKey() {
return key;
}
public long getFirstAccess() {
return firstAccess;
}
public long getLastAccess() {
return lastAccess;
}
public int getCount() {
return count;
}
public ImmutableMap<String, String> getMetadata() {
return metadata;
}
private SessionData touch(ImmutableMap<String, String> metadata) {
return new SessionData(key, firstAccess, System.currentTimeMillis(), System.nanoTime(), count + 1, metadata);
}
@Override
public String toString() {
return "SessionData{" +
"key='" + key + '\'' +
", firstAccess=" + firstAccess +
", lastAccess=" + lastAccess +
", metadata=" + metadata +
'}';
}
@Override
public int compareTo(SessionData o) {
return (int) (lastAccessNano - o.lastAccessNano);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SessionData that = (SessionData) o;
if (!key.equals(that.key)) return false;
return true;
}
@Override
public int hashCode() {
return key.hashCode();
}
}
private final ConcurrentMap<String, SessionData> sessions = Maps.newConcurrentMap();
private final int limit;
public Sessions(int limit) {
this.limit = limit;
}
public Optional<SessionData> get(String key) {
return Optional.fromNullable(sessions.get(key));
}
public ImmutableMap<String, SessionData> getAll() {
return ImmutableMap.copyOf(sessions);
}
public SessionData touch(String key, ImmutableMap<String, String> metadata) {
boolean updated = false;
SessionData updatedSessionData;
do {
SessionData sessionData;
sessionData = sessions.get(key);
if (sessionData != null) {
updatedSessionData = sessionData.touch(metadata);
} else {
long access = System.currentTimeMillis();
updatedSessionData = new SessionData(key, access, access, System.nanoTime(), 1, metadata);
}
updated = sessions.put(key, updatedSessionData) == sessionData;
} while (!updated);
// take size under limit
// note that it may exceed the limit for a short time until the following code completes
int size = sessions.size();
int remainingChecks = (size - limit) * 3 + 100;
while (sessions.size() > limit) {
if (remainingChecks-- == 0) {
// we have tried too many times to remove exceeding elements.
// the possible cause is that oldest element is always updated between we find it and try to remove it
// this is very unlikely but it's better to fail than run into an infinite loop
throw new IllegalStateException(
String.format(
"didn't manage to limit the size of sessions data within a reasonnable (%d) number of attempts",
(size - limit) * 3 + 100));
}
SessionData oldest = Ordering.natural().leastOf(sessions.values(), 1).get(0);
// we check if we still need to remove an element, the sessions may have changed while we were
// looking for the oldest element
if (sessions.size() > limit) {
// we remove it only if it hasn't changed. If it changed the remove method of ConcurrentMap won't
// remove it, and we will go on with the while loop
sessions.remove(oldest.getKey(), oldest);
}
}
return updatedSessionData;
}
}