/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* 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 org.iternine.jeppetto.dao.mongodb;
import org.iternine.jeppetto.dao.mongodb.enhance.DirtyableDBObject;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
class MongoDBSession {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private final Map<String, MongoDBSessionCache> caches = new HashMap<String, MongoDBSessionCache>();
private final Map<MongoDBQueryModelDAO<?, ?>, Map<DBObject, Object>> savedPerDAO = new HashMap<MongoDBQueryModelDAO<?, ?>, Map<DBObject, Object>>();
private final Map<MongoDBQueryModelDAO<?, ?>, Collection<DBObject>> deletedPerDAO = new HashMap<MongoDBQueryModelDAO<?, ?>, Collection<DBObject>>();
private final Deque<SessionEntryPoint> creators = new ArrayDeque<SessionEntryPoint>();
//-------------------------------------------------------------
// Variables - Private - Static
//-------------------------------------------------------------
private static final ThreadLocal<MongoDBSession> LOCAL = new ThreadLocal<MongoDBSession>();
private static final Logger logger = LoggerFactory.getLogger(MongoDBSession.class);
//-------------------------------------------------------------
// Methods - Package - Static
//-------------------------------------------------------------
static boolean isActive() {
return LOCAL.get() != null;
}
static void create() {
create(logger, "Unknown Context");
}
static void create(Logger contextLogger, String contextName) {
if (!isActive()) {
logger.debug("Creating new MongoDBSession.");
LOCAL.set(new MongoDBSession());
}
LOCAL.get().enter(contextLogger, contextName);
}
static void remove() {
if (isActive()) {
if (LOCAL.get().exit()) {
LOCAL.remove();
}
}
}
static <T, ID> void trackForSave(MongoDBQueryModelDAO<T, ID> mongoDBQueryModelDAO, DBObject identifier, T entity, DBObject... cacheKeys) {
validateState();
MongoDBSession mongoDBSession = LOCAL.get();
Map<DBObject, Object> savedEntities = mongoDBSession.savedPerDAO.get(mongoDBQueryModelDAO);
Collection<DBObject> deletedIdentifiers = mongoDBSession.deletedPerDAO.get(mongoDBQueryModelDAO);
if (savedEntities == null) {
savedEntities = new LinkedHashMap<DBObject, Object>();
mongoDBSession.savedPerDAO.put(mongoDBQueryModelDAO, savedEntities);
}
if (deletedIdentifiers != null && deletedIdentifiers.contains(identifier)) {
logger.debug("Item identified by {} has already been marked for delete, discarding.", identifier);
return;
}
logger.debug("Tracking for save: {} = {}", identifier, entity);
savedEntities.put(identifier, entity);
MongoDBSessionCache sessionCache = getCache(mongoDBQueryModelDAO.getDbCollection().getName());
for (DBObject cacheKey : cacheKeys) {
sessionCache.put(cacheKey, entity);
}
}
static void trackForDelete(MongoDBQueryModelDAO<?, ?> mongoDBQueryModelDAO, DBObject identifier) {
validateState();
MongoDBSession mongoDBSession = LOCAL.get();
Collection<DBObject> deletedEntityIdentifiers = mongoDBSession.deletedPerDAO.get(mongoDBQueryModelDAO);
if (logger.isDebugEnabled()) {
// TODO: check upsert list?
if (deletedEntityIdentifiers.contains(identifier)) {
logger.debug("Object already tracked for delete: {}", identifier);
} else {
logger.debug("Tracking for delete: {}", identifier);
}
}
deletedEntityIdentifiers.add(identifier);
if (mongoDBSession.savedPerDAO.get(mongoDBQueryModelDAO) != null) {
mongoDBSession.savedPerDAO.get(mongoDBQueryModelDAO).remove(identifier);
}
}
static void flush() {
validateState();
MongoDBSession mongoDBSession = LOCAL.get();
// check to see if this is a re-entrant session
if (mongoDBSession.creators.size() > 1) {
return;
}
try {
Set<MongoDBQueryModelDAO<?, ?>> daoSet = new HashSet<MongoDBQueryModelDAO<?, ?>>();
daoSet.addAll(mongoDBSession.savedPerDAO.keySet());
daoSet.addAll(mongoDBSession.deletedPerDAO.keySet());
for (MongoDBQueryModelDAO<?, ?> mongoDBQueryModelDAO : daoSet) {
mongoDBSession.doFlush(mongoDBQueryModelDAO);
}
} finally {
mongoDBSession.clear();
}
}
static void flush(MongoDBQueryModelDAO<?, ?> mongoDBQueryModelDAO) {
validateState();
LOCAL.get().doFlush(mongoDBQueryModelDAO);
}
static Object getObjectFromCache(String type, DBObject query) {
return getCache(type).get(query);
}
//-------------------------------------------------------------
// Methods - Private - Static
//-------------------------------------------------------------
private static void validateState() {
if (!isActive()) {
throw new IllegalStateException("Session not active.");
}
}
private static MongoDBSessionCache getCache(String type) {
validateState();
MongoDBSession mongoDBSession = LOCAL.get();
MongoDBSessionCache sessionCache = mongoDBSession.caches.get(type);
if (sessionCache != null) {
return sessionCache;
} else {
mongoDBSession.caches.put(type, new MongoDBSessionCache());
return mongoDBSession.caches.get(type);
}
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private void enter(Logger logger, String name) {
int offset = 3;
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
StackTraceElement creator = stackTrace[Math.min(offset, stackTrace.length - 1)];
creators.push(new SessionEntryPoint(creator, logger, name));
}
/**
* @return true if session can now removed from thread local storage
*/
private boolean exit() {
boolean last = creators.size() == 1;
try {
if (!last) {
String context = creators.peek().getName();
logger.debug("Leaving re-entrant session created by {}", context);
}
creators.pop();
if (last) {
logger.debug("Removing MongoDBSession");
if (logger.isDebugEnabled()) {
for (Map.Entry<MongoDBQueryModelDAO<?, ?>, Map<DBObject, Object>> updatedPerDAOEntry : savedPerDAO.entrySet()) {
for (Object entity : updatedPerDAOEntry.getValue().values()) {
if (entity instanceof DirtyableDBObject && ((DirtyableDBObject) entity).isDirty()) {
logger.warn("{} is still dirty: {}", updatedPerDAOEntry.getKey().getClass(), entity);
}
}
}
for (Collection<DBObject> deletedEntityIdentifiers : deletedPerDAO.values()) {
for (DBObject deletedEntityIdentifier : deletedEntityIdentifiers) {
logger.debug("Removing {} before delete due to session close.", deletedEntityIdentifier);
}
}
}
}
} catch (Throwable t) {
try {
logger.error("Error while removing session.", t);
} catch (Throwable t2) {
// bury
}
}
return last;
}
private void clear() {
savedPerDAO.clear();
caches.clear();
deletedPerDAO.clear();
}
private void doFlush(MongoDBQueryModelDAO<?, ?> mongoDBQueryModelDAO) {
String contextName = creators.peek().getName();
Logger contextLogger = creators.peek().getLogger();
long dirtyCheckCost = 0L;
int saveCount = 0;
int deleteCount = 0;
// TODO : perform actions in order they're tracked
if (savedPerDAO.containsKey(mongoDBQueryModelDAO)) {
Map<DBObject, Object> savedEntities = savedPerDAO.get(mongoDBQueryModelDAO);
for (Map.Entry<DBObject, Object> entry : savedEntities.entrySet()) {
try {
DirtyableDBObject enhancedEntity = (DirtyableDBObject) entry.getValue();
long beforeDirtyCheck = System.nanoTime();
boolean isDirty = enhancedEntity.isDirty();
dirtyCheckCost += (System.nanoTime() - beforeDirtyCheck);
if (!isDirty) {
continue;
}
mongoDBQueryModelDAO.trueSave(entry.getKey(), enhancedEntity);
saveCount++;
} catch (MongoException.DuplicateKey e) {
logger.warn("Error saving {}. Duplicate record found.", entry.getValue());
} catch (MongoException e) {
logger.error("Error saving {}.", entry.getValue(), e); // TODO: Implement
}
}
savedEntities.clear();
}
if (deletedPerDAO.containsKey(mongoDBQueryModelDAO)) {
Collection<DBObject> deletedEntityIdentifiers = deletedPerDAO.get(mongoDBQueryModelDAO);
for (DBObject deletedEntityIdentifier : deletedEntityIdentifiers) {
try {
mongoDBQueryModelDAO.trueRemove(deletedEntityIdentifier);
deleteCount++;
} catch (MongoException e) {
logger.error("Error removing {}.", deletedEntityIdentifier, e); // TODO: Implement
}
}
deletedEntityIdentifiers.clear();
}
getCache(mongoDBQueryModelDAO.getDbCollection().getName()).clear();
contextLogger.debug("{} flushed {}s in {}ms. (save={}, delete={})",
contextName, mongoDBQueryModelDAO.getCollectionClass().getSimpleName(),
TimeUnit.NANOSECONDS.toMillis(dirtyCheckCost), saveCount, deleteCount);
}
//-------------------------------------------------------------
// Inner Classes
//-------------------------------------------------------------
private static final class SessionEntryPoint {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private StackTraceElement stackTraceElement;
private Logger logger;
private String name;
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
public SessionEntryPoint(StackTraceElement stackTraceElement, Logger logger, String name) {
this.stackTraceElement = stackTraceElement;
this.logger = logger;
this.name = name;
}
//-------------------------------------------------------------
// Methods - Getters
//-------------------------------------------------------------
public StackTraceElement getStackTraceElement() {
return stackTraceElement;
}
public Logger getLogger() {
return logger;
}
public String getName() {
return name;
}
}
}