package net.contextfw.web.commons.cloud.session;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import net.contextfw.web.application.PageContext;
import net.contextfw.web.application.configuration.Configuration;
import net.contextfw.web.application.configuration.SettableProperty;
import net.contextfw.web.commons.cloud.binding.CloudDatabase;
import net.contextfw.web.commons.cloud.internal.mongo.MongoBase;
import net.contextfw.web.commons.cloud.internal.serializer.Serializer;
import net.contextfw.web.commons.cloud.internal.session.CloudSessionHolder;
import org.apache.commons.lang.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
/**
* Provides MongoDB-based cloud session handling.
*
* <p>
* This session handler requires that there exists a <code>DB</code>-instance that is bound to
* DI-container with annotation @CloudDatabase
* </p>
*
*/
@Singleton
public class MongoCloudSession extends MongoBase implements CloudSession {
private static final String NO_SESSION = "Cannot open session in EXISTING-mode! " +
"Session has not been initialized";
private static final String SESSION_NOT_OPEN = "Session is not open.";
private static class LocalData {
Map<String, Object> cache = new HashMap<String, Object>();
Set<String> changed = new HashSet<String>();
Set<String> removed = new HashSet<String>();
OpenMode openMode;
}
private final Provider<CloudSessionHolder> holderProvider;
private final ThreadLocal<LocalData> localData = new ThreadLocal<LocalData>() {
protected LocalData initialValue() {
return new LocalData();
}
};
private final Serializer serializer;
public static final SettableProperty<String> COLLECTION_NAME =
Configuration.createProperty(String.class,
MongoCloudSession.class.getCanonicalName() + ".collectionName");
private final Provider<PageContext> httpContext;
private static final long HALF_HOUR = 30*60*1000;
private final String cookieName;
private final String sessionCollection;
private final long maxInactivity;
@Inject
public MongoCloudSession(@CloudDatabase DB db,
Configuration configuration,
Provider<PageContext> httpContext,
Provider<CloudSessionHolder> holderProvider,
Serializer serializer) {
super(db, configuration.get(Configuration.REMOVAL_SCHEDULE_PERIOD));
this.httpContext = httpContext;
cookieName = configuration.getOrElse(CloudSession.COOKIE_NAME, "cloudSession");
sessionCollection = configuration.getOrElse(COLLECTION_NAME, "session");
this.maxInactivity = configuration.getOrElse(CloudSession.MAX_INACTIVITY, HALF_HOUR);
this.serializer = serializer;
this.holderProvider = holderProvider;
setIndexes(getCollection());
}
@Override
public void set(final String key, final Object value, boolean nonCached) {
assertSessionIsUsable();
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException("Key cannot be empty or null.");
}
final String handle = getSessionHandle(localData.get().openMode != OpenMode.EXISTING, true);
if (handle != null) {
if (value == null) {
unset(handle, key, nonCached);
}
LocalData data = localData.get();
data.removed.remove(key);
data.changed.add(key);
data.cache.put(key, value);
if (nonCached) {
DBObject query = o(KEY_HANDLE, handle);
DBObject update = o("$set", o(key, serializer.serialize(value)));
getCollection().update(query, update);
}
}
}
private boolean isSessionValid(String handle) {
DBObject query = b()
.add(KEY_HANDLE, handle)
.push(KEY_VALID_THROUGH)
.add("$gte", System.currentTimeMillis()).get();
return getCollection().count(query) > 0;
}
private String createSession() {
String handle = UUID.randomUUID().toString();
DBObject session = new BasicDBObject();
session.put(KEY_HANDLE, handle);
session.put(KEY_LOCKED, false);
session.put(KEY_VALID_THROUGH, System.currentTimeMillis() + maxInactivity);
getCollection().insert(session);
return handle;
}
private String getSessionHandle(boolean create, boolean assignCookie) {
CloudSessionHolder holder = this.holderProvider.get();
if (holder.getHandle() != null && holder.isOpen()) {
return holder.getHandle();
}
if (holder.getHandle() == null) {
holder.setHandle(findHandleFromCookie());
}
if (holder.getHandle() != null && !isSessionValid(holder.getHandle())) {
holder.setHandle(null);
}
if (holder.getHandle() == null && create) {
if (httpContext.get().getResponse() != null) {
holder.setHandle(createSession());
if (assignCookie) {
setSessionCookie(holder.getHandle(), false);
}
} else {
throw new NoSessionException(
"Cannot create new session! " +
"HttpResponse not bound");
}
}
return holder.getHandle();
}
private String typeToKey(Class<?> type) {
return type.getName().replaceAll("\\.", "");
}
@Override
public void set(Object value) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null.");
}
set(typeToKey(value.getClass()), value, false);
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(final String key, Class<T> type, boolean nonCached) {
assertSessionIsUsable();
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException("Key cannot be empty or null.");
}
if (type == null) {
throw new IllegalArgumentException("Type cannot be empty or null.");
}
LocalData localData = getLocalData();
if (localData.removed.contains(key)) {
return null;
} else if (!nonCached && localData.cache.containsKey(key)) {
return (T) localData.cache.get(key);
}
final String handle = getSessionHandle(false, false);
if (handle != null) {
DBObject query = o(KEY_HANDLE, handle);
DBObject field = o(key, 1);
DBObject obj = getCollection().findOne(query, field);
byte[] data = (byte[]) (obj == null ? null : obj.get(key));
T rv = data == null ? null : (T) serializer.unserialize(data);
localData.cache.put(key, rv);
return rv;
} else {
return null;
}
}
private LocalData getLocalData() {
return localData.get();
}
@Override
public <T> T get(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be empty or null.");
}
return get(typeToKey(type), type, false);
}
private void assertSessionIsUsable() {
CloudSessionHolder holder = holderProvider.get();
if (!holder.isOpen()) {
throw new NoSessionException(SESSION_NOT_OPEN);
}
}
@Override
public void remove(String key, boolean nonCached) {
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException("Key cannot be empty or null.");
}
String handle = getSessionHandle(false, false);
if (handle != null) {
unset(handle, key, nonCached);
}
}
private void unset(final String handle, final String key, boolean nonCached) {
assertSessionIsUsable();
LocalData localData = this.localData.get();
localData.cache.remove(key);
localData.changed.remove(key);
localData.removed.add(key);
if (nonCached) {
DBObject query = o(KEY_HANDLE, handle);
DBObject update = o("$unset", o(key, 1));
getCollection().update(query, update);
}
}
@Override
public void remove(Class<?> type) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be empty or null.");
}
remove(typeToKey(type));
}
@Override
public void expireSession() {
String handle = getSessionHandle(false, false);
localData.remove();
if (handle != null) {
setSessionCookie(handle, true);
removeSession(handle);
}
}
private void removeSession(String handle) {
getCollection().remove(o(KEY_HANDLE, handle));
}
private String findHandleFromCookie() {
HttpServletRequest request = httpContext.get().getRequest();
if (request != null) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
}
return null;
}
@Override
public void openSession(OpenMode mode) {
localData.remove();
localData.get().openMode = mode;
removeExpiredSessions();
CloudSessionHolder holder = this.holderProvider.get();
if (httpContext.get().getRequest() == null && mode != OpenMode.EXISTING) {
throw new NoSessionException(
"Cannot open session in " + mode + "-mode! " +
"No request bound to PageContext. Use EXISTING-mode instead");
}
String handle = getSessionHandle(mode == OpenMode.EAGER, false);
if (holder.getHandle() == null && mode == OpenMode.EXISTING) {
throw new NoSessionException(NO_SESSION);
}
holder.setOpen(true);
if (handle != null && mode != OpenMode.EXISTING) {
setSessionCookie(handle, false);
refreshSession(handle);
}
if (holder.getHandle() == null && mode == OpenMode.EXISTING) {
throw new NoSessionException(NO_SESSION);
}
}
private void setSessionCookie(String handle, boolean remove) {
if (httpContext.get().getResponse() != null) {
Cookie cookie = new Cookie(cookieName, handle);
cookie.setPath("/");
cookie.setMaxAge(remove ? 0 : (int) (maxInactivity/1000));
httpContext.get().getResponse().addCookie(cookie);
}
}
private void refreshSession(String handle) {
getCollection().update(o(KEY_HANDLE, handle),
o("$set",
o(KEY_VALID_THROUGH,
System.currentTimeMillis() + maxInactivity)));
}
@Override
protected DBCollection getCollection() {
return getDb().getCollection(sessionCollection);
}
private void removeExpiredSessions() {
removeExpiredObjects();
}
@Override
public void closeSession() {
final String handle = getSessionHandle(localData.get().openMode != OpenMode.EXISTING, false);
final LocalData ld = localData.get();
if (!ld.changed.isEmpty() || !ld.removed.isEmpty()) {
DBObject query = o(KEY_HANDLE, handle);
BasicDBObjectBuilder update = b();
if (!ld.removed.isEmpty()) {
update.push("$unset");
for (String _ : ld.removed) {
update.add(_, 1);
}
update.pop();
}
if (!ld.changed.isEmpty()) {
update.push("$set");
for (String _ : ld.changed) {
update.add(_, serializer.serialize(ld.cache.get(_)));
}
update.pop();
}
getCollection().update(query, update.get());
}
localData.remove();
holderProvider.get().setOpen(false);
}
@Override
public void set(String key, Object value) {
set(key, value, false);
}
@Override
public void set(Object value, boolean nonCached) {
if (value == null) {
throw new IllegalArgumentException("Value cannot be null.");
}
set(typeToKey(value.getClass()), value, nonCached);
}
@Override
public <T> T get(String key, Class<T> type) {
return get(key, type, false);
}
@Override
public <T> T get(Class<T> type, boolean nonCached) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be empty or null.");
}
return get(typeToKey(type), type, nonCached);
}
@Override
public void remove(String key) {
remove(key, false);
}
@Override
public void remove(Class<?> type, boolean nonCached) {
if (type == null) {
throw new IllegalArgumentException("Type cannot be empty or null.");
}
remove(typeToKey(type), nonCached);
}
}