From 710198833cab54516d940326cd3216cfebe81b90 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 13 Mar 2017 18:30:52 +0100 Subject: [PATCH] #1125 Create SingleFilePersistenceHandler + extract (de)serializer for LimboPlayer --- .../persistence/LimboPersistenceType.java | 2 + .../persistence/LimboPlayerDeserializer.java | 115 ++++++++++++++++++ .../persistence/LimboPlayerSerializer.java | 52 ++++++++ .../SeparateFilePersistenceHandler.java | 92 +------------- .../SingleFilePersistenceHandler.java | 94 ++++++++++++++ .../settings/properties/LimboSettings.java | 5 +- 6 files changed, 268 insertions(+), 92 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java create mode 100644 src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java create mode 100644 src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java index 9e6d8fe9..294ccbff 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java @@ -7,6 +7,8 @@ public enum LimboPersistenceType { INDIVIDUAL_FILES(SeparateFilePersistenceHandler.class), + SINGLE_FILE(SingleFilePersistenceHandler.class), + DISABLED(NoOpPersistenceHandler.class); private final Class implementationClass; diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java new file mode 100644 index 00000000..94e1950b --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java @@ -0,0 +1,115 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.Location; +import org.bukkit.World; + +import java.lang.reflect.Type; +import java.util.function.Function; + +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUP; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOCATION; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_PITCH; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_WORLD; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_X; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED; + +/** + * Converts a JsonElement to a LimboPlayer. + */ +class LimboPlayerDeserializer implements JsonDeserializer { + + private BukkitService bukkitService; + + LimboPlayerDeserializer(BukkitService bukkitService) { + this.bukkitService = bukkitService; + } + + @Override + public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + if (jsonObject == null) { + return null; + } + + Location loc = deserializeLocation(jsonObject); + boolean operator = getBoolean(jsonObject, IS_OP); + String group = getString(jsonObject, GROUP); + boolean canFly = getBoolean(jsonObject, CAN_FLY); + float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED); + float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED); + + return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed); + } + + private Location deserializeLocation(JsonObject jsonObject) { + JsonElement e; + if ((e = jsonObject.getAsJsonObject(LOCATION)) != null) { + JsonObject locationObject = e.getAsJsonObject(); + World world = bukkitService.getWorld(getString(locationObject, LOC_WORLD)); + if (world != null) { + double x = getDouble(locationObject, LOC_X); + double y = getDouble(locationObject, LOC_Y); + double z = getDouble(locationObject, LOC_Z); + float yaw = getFloat(locationObject, LOC_YAW); + float pitch = getFloat(locationObject, LOC_PITCH); + return new Location(world, x, y, z, yaw, pitch); + } + } + return null; + } + + private static String getString(JsonObject jsonObject, String memberName) { + JsonElement element = jsonObject.get(memberName); + return element != null ? element.getAsString() : ""; + } + + private static boolean getBoolean(JsonObject jsonObject, String memberName) { + JsonElement element = jsonObject.get(memberName); + return element != null && element.getAsBoolean(); + } + + private static float getFloat(JsonObject jsonObject, String memberName) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, 0.0f); + } + + private static float getFloat(JsonObject jsonObject, String memberName, float defaultValue) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, defaultValue); + } + + private static double getDouble(JsonObject jsonObject, String memberName) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsDouble, 0.0); + } + + /** + * Gets a number from the given JsonElement safely. + * + * @param jsonElement the element to retrieve the number from + * @param numberFunction the function to get the number from the element + * @param defaultValue the value to return if the element is null or the number cannot be retrieved + * @param the number type + * @return the number from the given JSON element, or the default value + */ + private static N getNumberFromElement(JsonElement jsonElement, + Function numberFunction, + N defaultValue) { + if (jsonElement != null) { + try { + return numberFunction.apply(jsonElement); + } catch (NumberFormatException ignore) { + } + } + return defaultValue; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java new file mode 100644 index 00000000..aeae3b65 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java @@ -0,0 +1,52 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import fr.xephi.authme.data.limbo.LimboPlayer; +import org.bukkit.Location; + +import java.lang.reflect.Type; + +/** + * Converts a LimboPlayer to a JsonElement. + */ +class LimboPlayerSerializer implements JsonSerializer { + + static final String LOCATION = "location"; + static final String LOC_WORLD = "world"; + static final String LOC_X = "x"; + static final String LOC_Y = "y"; + static final String LOC_Z = "z"; + static final String LOC_YAW = "yaw"; + static final String LOC_PITCH = "pitch"; + + static final String GROUP = "group"; + static final String IS_OP = "operator"; + static final String CAN_FLY = "can-fly"; + static final String WALK_SPEED = "walk-speed"; + static final String FLY_SPEED = "fly-speed"; + + + @Override + public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) { + Location loc = limboPlayer.getLocation(); + JsonObject locationObject = new JsonObject(); + locationObject.addProperty(LOC_WORLD, loc.getWorld().getName()); + locationObject.addProperty(LOC_X, loc.getX()); + locationObject.addProperty(LOC_Y, loc.getY()); + locationObject.addProperty(LOC_Z, loc.getZ()); + locationObject.addProperty(LOC_YAW, loc.getYaw()); + locationObject.addProperty(LOC_PITCH, loc.getPitch()); + + JsonObject obj = new JsonObject(); + obj.add(LOCATION, locationObject); + obj.addProperty(GROUP, limboPlayer.getGroup()); + obj.addProperty(IS_OP, limboPlayer.isOperator()); + obj.addProperty(CAN_FLY, limboPlayer.isCanFly()); + obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed()); + obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed()); + return obj; + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java index 60927cf0..438bce69 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SeparateFilePersistenceHandler.java @@ -3,26 +3,17 @@ package fr.xephi.authme.data.limbo.persistence; import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.limbo.LimboPlayer; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.util.FileUtils; import fr.xephi.authme.util.PlayerUtils; -import org.bukkit.Location; -import org.bukkit.World; import org.bukkit.entity.Player; import javax.inject.Inject; import java.io.File; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; /** @@ -32,19 +23,16 @@ class SeparateFilePersistenceHandler implements LimboPersistenceHandler { private final Gson gson; private final File cacheDir; - private final BukkitService bukkitService; @Inject SeparateFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) { - this.bukkitService = bukkitService; - cacheDir = new File(dataFolder, "playerdata"); if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) { - ConsoleLogger.warning("Failed to create userdata directory."); + ConsoleLogger.warning("Failed to create playerdata directory '" + cacheDir + "'"); } gson = new GsonBuilder() .registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer()) - .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer()) + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService)) .setPrettyPrinting() .create(); } @@ -99,80 +87,4 @@ class SeparateFilePersistenceHandler implements LimboPersistenceHandler { public LimboPersistenceType getType() { return LimboPersistenceType.INDIVIDUAL_FILES; } - - private final class LimboPlayerDeserializer implements JsonDeserializer { - - @Override - public LimboPlayer deserialize(JsonElement jsonElement, Type type, - JsonDeserializationContext context) { - JsonObject jsonObject = jsonElement.getAsJsonObject(); - if (jsonObject == null) { - return null; - } - - Location loc = null; - String group = ""; - boolean operator = false; - boolean canFly = false; - float walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED; - float flySpeed = LimboPlayer.DEFAULT_FLY_SPEED; - - JsonElement e; - if ((e = jsonObject.getAsJsonObject("location")) != null) { - JsonObject obj = e.getAsJsonObject(); - World world = bukkitService.getWorld(obj.get("world").getAsString()); - if (world != null) { - double x = obj.get("x").getAsDouble(); - double y = obj.get("y").getAsDouble(); - double z = obj.get("z").getAsDouble(); - float yaw = obj.get("yaw").getAsFloat(); - float pitch = obj.get("pitch").getAsFloat(); - loc = new Location(world, x, y, z, yaw, pitch); - } - } - if ((e = jsonObject.get("group")) != null) { - group = e.getAsString(); - } - if ((e = jsonObject.get("operator")) != null) { - operator = e.getAsBoolean(); - } - if ((e = jsonObject.get("can-fly")) != null) { - canFly = e.getAsBoolean(); - } - if ((e = jsonObject.get("walk-speed")) != null) { - walkSpeed = e.getAsFloat(); - } - if ((e = jsonObject.get("fly-speed")) != null) { - flySpeed = e.getAsFloat(); - } - - return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed); - } - } - - private static final class LimboPlayerSerializer implements JsonSerializer { - - @Override - public JsonElement serialize(LimboPlayer limboPlayer, Type type, - JsonSerializationContext context) { - JsonObject obj = new JsonObject(); - obj.addProperty("group", limboPlayer.getGroup()); - - Location loc = limboPlayer.getLocation(); - JsonObject obj2 = new JsonObject(); - obj2.addProperty("world", loc.getWorld().getName()); - obj2.addProperty("x", loc.getX()); - obj2.addProperty("y", loc.getY()); - obj2.addProperty("z", loc.getZ()); - obj2.addProperty("yaw", loc.getYaw()); - obj2.addProperty("pitch", loc.getPitch()); - obj.add("location", obj2); - - obj.addProperty("operator", limboPlayer.isOperator()); - obj.addProperty("can-fly", limboPlayer.isCanFly()); - obj.addProperty("walk-speed", limboPlayer.getWalkSpeed()); - obj.addProperty("fly-speed", limboPlayer.getFlySpeed()); - return obj; - } - } } diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java new file mode 100644 index 00000000..57aa2dcf --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SingleFilePersistenceHandler.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.util.FileUtils; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Saves all LimboPlayers in one JSON file and keeps the entries in memory. + */ +class SingleFilePersistenceHandler implements LimboPersistenceHandler { + + private final File cacheFile; + private final Gson gson; + private Map entries; + + @Inject + SingleFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) { + cacheFile = new File(dataFolder, "limbo.json"); + if (!cacheFile.exists()) { + FileUtils.create(cacheFile); + } + + gson = new GsonBuilder() + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer()) + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService)) + .setPrettyPrinting() + .create(); + + Type type = new TypeToken>(){}.getType(); + try (FileReader fr = new FileReader(cacheFile)) { + entries = gson.fromJson(fr, type); + } catch (IOException e) { + ConsoleLogger.logException("Failed to read from '" + cacheFile + "':", e); + } + + if (entries == null) { + entries = new ConcurrentHashMap<>(); + } + } + + @Override + public LimboPlayer getLimboPlayer(Player player) { + return entries.get(PlayerUtils.getUUIDorName(player)); + } + + @Override + public void saveLimboPlayer(Player player, LimboPlayer limbo) { + entries.put(PlayerUtils.getUUIDorName(player), limbo); + saveEntries("adding '" + player.getName() + "'"); + } + + @Override + public void removeLimboPlayer(Player player) { + LimboPlayer entry = entries.remove(PlayerUtils.getUUIDorName(player)); + if (entry != null) { + saveEntries("removing '" + player.getName() + "'"); + } + } + + /** + * Saves the entries to the disk. + * + * @param action the reason for saving (for logging purposes) + */ + private void saveEntries(String action) { + try (FileWriter fw = new FileWriter(cacheFile)) { + gson.toJson(entries, fw); + } catch (IOException e) { + ConsoleLogger.logException("Failed saving JSON limbo cache after " + action, e); + } + } + + @Override + public LimboPersistenceType getType() { + return LimboPersistenceType.SINGLE_FILE; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java b/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java index 1e9d80d5..1d7a29d7 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java @@ -22,10 +22,11 @@ public final class LimboSettings implements SettingsHolder { "Besides storing the data in memory, you can define if/how the data should be persisted", "on disk. This is useful in case of a server crash, so next time the server starts we can", "properly restore things like OP status, ability to fly, and walk/fly speed.", - "DISABLED: no disk storage, INDIVIDUAL_FILES: each player data in its own file" + "DISABLED: no disk storage, INDIVIDUAL_FILES: each player data in its own file,", + "SINGLE_FILE: all data in one single file (only if you have a small server!)" }) public static final Property LIMBO_PERSISTENCE_TYPE = - newProperty(LimboPersistenceType.class, "limbo.persistence", LimboPersistenceType.INDIVIDUAL_FILES); + newProperty(LimboPersistenceType.class, "limbo.persistence.type", LimboPersistenceType.INDIVIDUAL_FILES); @Comment({ "Whether the player is allowed to fly: RESTORE, ENABLE, DISABLE.",