package net.t7seven7t.swornguard.io; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.logging.Level; import net.dmulloy2.io.FileSerialization; import net.dmulloy2.io.IOUtil; import net.dmulloy2.io.UUIDFetcher; import net.dmulloy2.types.Versioning; import net.dmulloy2.types.Versioning.Version; import net.dmulloy2.util.Util; import net.t7seven7t.swornguard.SwornGuard; import net.t7seven7t.swornguard.types.PlayerData; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; /** * @author dmulloy2 */ public class PlayerDataCache implements PlayerDataServiceProvider { private final File folder; private final String extension = ".dat"; private final String folderName = "players"; private final SwornGuard plugin; private final ConcurrentMap<String, PlayerData> cache; public PlayerDataCache(SwornGuard plugin) { this.folder = new File(plugin.getDataFolder(), folderName); if (! folder.exists()) folder.mkdirs(); this.cache = new ConcurrentHashMap<String, PlayerData>(64, 0.75F, 64); this.plugin = plugin; this.convertToUUID(); } // ---- Data Getters private final PlayerData getData(String key) { // Check cache first PlayerData data = cache.get(key); if (data == null) { // Attempt to load it File file = new File(folder, getFileName(key)); if (file.exists()) { data = loadData(key); if (data == null) { // Corrupt data :( if (! file.renameTo(new File(folder, file.getName() + "_bad"))) file.delete(); return null; } // Cache it cache.put(key, data); } } return data; } @Override public final PlayerData getData(UUID uniqueId) { return getData(uniqueId.toString()); } @Override public final PlayerData getData(Player player) { PlayerData data = getData(getKey(player)); // Online players should always have data if (data == null) { data = newData(player); } // Try to fetch history from Essentials List<String> history = data.getHistory(); if (history == null && plugin.isEssentialsEnabled()) { history = plugin.getEssentialsHandler().getHistory(player.getUniqueId()); } // Account for name changes String lastKnownBy = data.getLastKnownBy(); if (lastKnownBy != null && ! lastKnownBy.isEmpty()) { if (! lastKnownBy.equals(player.getName())) { if (history == null) { history = new ArrayList<String>(); } // Ensure we have the right casing if (lastKnownBy.equalsIgnoreCase(player.getName())) { plugin.getLogHandler().log("Corrected casing for {0}''s name.", player.getName()); history.remove(lastKnownBy); data.setLastKnownBy(lastKnownBy = player.getName()); history.add(lastKnownBy); } else { // Name change! plugin.getLogHandler().log("{0} changed their name to {1}.", lastKnownBy, player.getName()); data.setLastKnownBy(lastKnownBy = player.getName()); history.add(lastKnownBy); } } } else { data.setLastKnownBy(lastKnownBy = player.getName()); } if (history == null) { history = new ArrayList<String>(); history.add(player.getName()); } data.setHistory(history); data.setUniqueId(player.getUniqueId().toString()); // Return return data; } @Override public final PlayerData getData(OfflinePlayer player) { // Slightly different handling for Players if (player.isOnline()) return getData(player.getPlayer()); // Attempt to get by name return getData(getKey(player)); } // ---- Data Management public final PlayerData newData(String key) { // Construct PlayerData data = new PlayerData(); // Cache and return cache.put(key, data); return data; } public final PlayerData newData(Player player) { return newData(getKey(player)); } public final PlayerData newData(OfflinePlayer player) { return newData(getKey(player)); } private final PlayerData loadData(String key) { File file = new File(folder, getFileName(key)); try { PlayerData data = FileSerialization.load(file, PlayerData.class); if (Versioning.getVersion() != Version.MC_16) { data.setUniqueId(key); } return data; } catch (Throwable ex) { plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "loading data for {0}", key)); return null; } } public final void save() { long start = System.currentTimeMillis(); plugin.getLogHandler().log("Saving players to disk..."); for (Entry<String, PlayerData> entry : getAllLoadedPlayerData().entrySet()) { try { File file = new File(folder, getFileName(entry.getKey())); FileSerialization.save(entry.getValue(), file); } catch (Throwable ex) { plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "saving data for {0}", entry.getKey())); } } plugin.getLogHandler().log("Players saved. Took {0} ms.", System.currentTimeMillis() - start); } // Legacy @Deprecated public final void save(boolean cleanup) { save(); if (cleanup) cleanupData(); } public final void cleanupData() { // Get all online players into an array list List<String> online = new ArrayList<String>(); for (Player player : Util.getOnlinePlayers()) online.add(player.getName()); // Actually cleanup the data for (String key : getAllLoadedPlayerData().keySet()) if (! online.contains(key)) cache.remove(key); // Clear references online.clear(); online = null; } // ---- Mass Getters @Override public final Map<String, PlayerData> getAllLoadedPlayerData() { return Collections.unmodifiableMap(cache); } @Override public final Map<String, PlayerData> getAllPlayerData() { Map<String, PlayerData> data = new HashMap<String, PlayerData>(); data.putAll(cache); File[] files = folder.listFiles(); if (files == null || files.length == 0) { return Collections.unmodifiableMap(data); } for (File file : files) { if (file.getName().contains(extension)) { String fileName = IOUtil.trimFileExtension(file, extension); if (! isFileLoaded(fileName)) data.put(fileName, loadData(fileName)); } } return Collections.unmodifiableMap(data); } // ---- UUID Conversion private final void convertToUUID() { File updated = new File(folder, ".updated"); if (updated.exists()) { return; } if (Versioning.getVersion() == Version.MC_16) { return; } long start = System.currentTimeMillis(); plugin.getLogHandler().log("Checking for unconverted files"); Map<String, PlayerData> data = getUnconvertedData(); if (data.isEmpty()) { try { updated.createNewFile(); } catch (Throwable ex) { } return; } plugin.getLogHandler().log("Converting {0} files!", data.size()); try { List<String> names = new ArrayList<String>(data.keySet()); ImmutableList.Builder<List<String>> builder = ImmutableList.builder(); int namesCopied = 0; while (namesCopied < names.size()) { builder.add(ImmutableList.copyOf(names.subList(namesCopied, Math.min(namesCopied + 100, names.size())))); namesCopied += 100; } List<UUIDFetcher> fetchers = new ArrayList<UUIDFetcher>(); for (List<String> namesList : builder.build()) { fetchers.add(new UUIDFetcher(namesList)); } ExecutorService e = Executors.newFixedThreadPool(3); List<Future<Map<String, UUID>>> results = e.invokeAll(fetchers); File archive = new File(folder.getParentFile(), "archive"); if (! archive.exists()) archive.mkdir(); for (Future<Map<String, UUID>> result : results) { Map<String, UUID> uuids = result.get(); for (Entry<String, UUID> entry : uuids.entrySet()) { try { // Get and update String name = entry.getKey(); String uniqueId = entry.getValue().toString(); PlayerData dat = data.get(name); dat.setLastKnownBy(name); // Archive the old file File file = new File(folder, getFileName(name)); Files.move(file, new File(archive, file.getName())); // Create and save new file File newFile = new File(folder, getFileName(uniqueId)); FileSerialization.save(dat, newFile); } catch (Throwable ex) { plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "converting {0}", entry.getKey())); } } } } catch (Throwable ex) { plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "converting to UUID-based lookups!")); return; } plugin.getLogHandler().log("Successfully converted to UUID-based lookups! Took {0} ms!", System.currentTimeMillis() - start); } private final Map<String, PlayerData> getUnconvertedData() { Map<String, PlayerData> data = new HashMap<String, PlayerData>(); File[] files = folder.listFiles(new FileFilter() { @Override public boolean accept(File file) { String name = file.getName(); return name.contains(extension) && name.length() != 40; } }); for (File file : files) { try { PlayerData loaded = FileSerialization.load(file, PlayerData.class); if (loaded != null) { String fileName = IOUtil.trimFileExtension(file, extension); loaded.setLastKnownBy(fileName); data.put(fileName, loaded); } } catch (Throwable ex) { plugin.getLogHandler().log(Level.WARNING, Util.getUsefulStack(ex, "loading data {0}", file)); } } return Collections.unmodifiableMap(data); } // ---- Util private final String getKey(OfflinePlayer player) { if (Versioning.getVersion() == Version.MC_16) { return player.getName(); } return player.getUniqueId().toString(); } private final String getFileName(String key) { return key + extension; } private final boolean isFileLoaded(String fileName) { return cache.keySet().contains(fileName); } public int getFileListSize() { return folder.listFiles().length; } public int getCacheSize() { return cache.size(); } }