package com.nicewuerfel.blockown.database;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.nicewuerfel.blockown.Ownable;
import com.nicewuerfel.blockown.User;
import org.bukkit.Bukkit;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
class CacheAccessor implements AutoCloseable {
/**
* Estimated amount of blocks one player will place.
*/
private static final int BLOCKS_PER_PLAYER = 1024;
private static final int DEFAULT_START_CAPACITY = 65536;
private static final int CACHE_TIME = 15;
private final File dumpFile;
private final CachedDatabase db;
private final ScheduledExecutorService scheduler;
private final LoadingCache<Ownable, Optional<User>> cache;
CacheAccessor(CachedDatabase database, File pluginFolder) {
this.db = database;
this.scheduler = Executors.newSingleThreadScheduledExecutor();
this.cache = CacheBuilder.newBuilder().maximumSize(calculateCacheCapacity())
.expireAfterAccess(CACHE_TIME, TimeUnit.MINUTES)
.build(new CacheLoader<Ownable, Optional<User>>() {
@Override
public Optional<User> load(Ownable ownable) throws Exception {
return db.getDatabaseOwner(ownable);
}
});
dumpFile = new File(pluginFolder.getAbsoluteFile(), "dumpFile.dat");
if (dumpFile.exists()) {
Map<Ownable, Optional<User>> restored = restoreDumpedCache();
cache.putAll(restored);
}
}
/**
* Creates a new HashMap with an appropriate capacity.
*
* @return the HashMap
*/
private Map<Ownable, Optional<User>> initializeCache() {
return new ConcurrentHashMap<>(calculateCacheCapacity());
}
private int calculateCacheCapacity() {
int initialCapacity;
try {
initialCapacity = Bukkit.getOfflinePlayers().length * BLOCKS_PER_PLAYER;
} catch (NullPointerException e) {
// Neccessary for testing
initialCapacity = 0;
}
if (initialCapacity == 0) {
initialCapacity = DEFAULT_START_CAPACITY;
}
return initialCapacity;
}
/**
* Restores cacheAccessor from dump file and deletes file afterwards. If unsuccessful, returns
* empty cacheAccessor
*
* @return the restored cacheAccessor
*/
@SuppressWarnings("unchecked")
private Map<Ownable, Optional<User>> restoreDumpedCache() {
Map<Ownable, Optional<User>> result = null;
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dumpFile));
Object object = ois.readObject();
ois.close();
if (object instanceof Map<?, ?>) {
result = (Map<Ownable, Optional<User>>) object;
}
dumpFile.delete();
} catch (ClassNotFoundException | IOException e) {
db.getOutput().printException("Error restoring dumped cacheAccessor.", e);
if (!dumpFile.delete()) {
db.getOutput().printError("Couldn't delete corrupted cache dump, please delete "
+ dumpFile.getAbsolutePath() + " manually", e);
}
}
return (result == null) ? initializeCache() : result;
}
@Nonnull
Optional<User> getOwner(Ownable ownable) {
try {
return cache.get(ownable);
} catch (ExecutionException e) {
db.getOutput().printException("ExecutionException in CacheAccessor", e);
return Optional.absent();
}
}
boolean doAction(DatabaseAction databaseAction) {
switch (databaseAction.getActionType()) {
case UNOWN:
cache.put(databaseAction.getOwnable(), Optional.<User>absent());
doActionOnDatabase(databaseAction);
return true;
case OWN:
cache.put(databaseAction.getOwnable(), Optional.of(databaseAction.getUser()));
doActionOnDatabase(databaseAction);
return true;
case DROP:
dropUserData(databaseAction.getUser());
return true;
default:
db.getOutput().printException(new IllegalArgumentException("Invalid DatabaseActionType"));
return false;
}
}
private void doActionOnDatabase(final DatabaseAction databaseAction) {
if (!db.setDatabaseOwner(databaseAction)) {
db.getOutput().printConsole("Couldn't perform database action, retrying in 1 second...");
getScheduler().schedule(new Runnable() {
@Override
public void run() {
if (!db.setDatabaseOwner(databaseAction)) {
db.getOutput().printConsole(
"Second try failed too, not performing the action. Please report this issue!");
}
}
}, 1, TimeUnit.SECONDS);
}
}
private void dropUserData(User user) {
if (user == null) {
return;
}
Iterator<Entry<Ownable, Optional<User>>> iterator = cache.asMap().entrySet().iterator();
Entry<Ownable, Optional<User>> entry;
while (iterator.hasNext()) {
entry = iterator.next();
if (entry.getValue().isPresent() && user.equals(entry.getValue().get())) {
cache.put(entry.getKey(), Optional.<User>absent());
}
}
db.dropDatabaseUserData(user);
}
@Nonnull
private ScheduledExecutorService getScheduler() {
return scheduler;
}
@Override
public void close() {
getScheduler().shutdownNow();
}
}