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(); } }