From 8b6fe63df1a5eb5e1eac83c7ff871c98dd754b3e Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Sat, 7 Oct 2023 19:42:26 +0200 Subject: [PATCH] Proper serialization of ItemStack and other Serializable stuff in Bukkit API --- .../java/fr/pandacube/lib/core/json/Json.java | 1 + .../ConfigurationSerializableAdapter.java | 79 +++++++++++++++++++ .../lib/paper/json/ItemStackAdapter.java | 68 ++++++++++++---- .../pandacube/lib/paper/json/PaperJson.java | 1 + 4 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ConfigurationSerializableAdapter.java diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java index 7646abc..e4ff7bb 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java @@ -52,6 +52,7 @@ public class Json { private static Gson build(Function builderModifier) { GsonBuilder base = new GsonBuilder() .registerTypeAdapterFactory(new CustomAdapterFactory()) + .disableHtmlEscaping() .setLenient(); return builderModifier.apply(base).create(); } diff --git a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ConfigurationSerializableAdapter.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ConfigurationSerializableAdapter.java new file mode 100644 index 0000000..376e805 --- /dev/null +++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ConfigurationSerializableAdapter.java @@ -0,0 +1,79 @@ +package fr.pandacube.lib.paper.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.bind.TreeTypeAdapter; +import com.google.gson.reflect.TypeToken; +import fr.pandacube.lib.core.json.Json; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.BlockVector; +import org.yaml.snakeyaml.Yaml; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * Gson adapter for ConfigurationSerializable, an interface implemented by several classes in the Bukkit API to ease + * serialization to YAML. + * + * To not reinvent the wheel, this class uses the Bukkit’s Yaml API to convert the objects from/to json. + */ +/* package */ class ConfigurationSerializableAdapter implements JsonSerializer, JsonDeserializer { + + public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(ConfigurationSerializable.class, new ConfigurationSerializableAdapter()); + + private static final TypeToken> MAP_STR_OBJ_TYPE = new TypeToken<>() { }; + + + private boolean isItemStack(Map deserializedMap) { + return deserializedMap.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY) + && deserializedMap.get(ConfigurationSerialization.SERIALIZED_TYPE_KEY) instanceof String serializedType + && ItemStack.class.equals(ConfigurationSerialization.getClassByAlias(serializedType)); + } + + @Override + public ConfigurationSerializable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!(json instanceof JsonObject jsonObj) || !jsonObj.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) + throw new JsonParseException("Unable to deserialize a ConfigurationSerializable from the provided json structure."); + Map map = context.deserialize(jsonObj, MAP_STR_OBJ_TYPE.getType()); + if (isItemStack(map)) { + ItemStackAdapter.fixDeserializationVersion(map); + } + String yaml = new Yaml().dump(Map.of("obj", map)); + YamlConfiguration cfg = new YamlConfiguration(); + try { + cfg.loadFromString(yaml); + } catch (InvalidConfigurationException e) { + throw new JsonParseException("Unable t deserialize a ConfigurationSerializable from the provided json structure.", e); + } + return cfg.getSerializable("obj", ConfigurationSerializable.class); + } + + @Override + public JsonElement serialize(ConfigurationSerializable src, Type typeOfSrc, JsonSerializationContext context) { + YamlConfiguration cfg = new YamlConfiguration(); + cfg.set("obj", src); + Map map = new Yaml().load(cfg.saveToString()); + return context.serialize(map.get("obj"), MAP_STR_OBJ_TYPE.getType()); + } + + + public static void main(String[] args) { + PaperJson.init(); + BlockVector bv = new BlockVector(12, 24, 48); + String json = Json.gson.toJson(bv); + System.out.println(json); + BlockVector bv2 = Json.gson.fromJson(json, BlockVector.class); + System.out.println(bv.equals(bv2)); + } +} diff --git a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ItemStackAdapter.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ItemStackAdapter.java index 3341b9e..f530988 100644 --- a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ItemStackAdapter.java +++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/ItemStackAdapter.java @@ -1,8 +1,11 @@ package fr.pandacube.lib.paper.json; +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.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; @@ -10,7 +13,10 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.bind.TreeTypeAdapter; import com.google.gson.reflect.TypeToken; import org.bukkit.Bukkit; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import java.lang.reflect.Type; import java.util.Map; @@ -21,30 +27,62 @@ import java.util.Map; private static final TypeToken> MAP_STR_OBJ_TYPE = new TypeToken<>() { }; + /** Gson instance with no custom type adapter */ + private static final Gson vanillaGson = new GsonBuilder().setLenient().create(); @Override public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - Map deserializedMap = context.deserialize(json, MAP_STR_OBJ_TYPE.getType()); - int itemStackVersion = deserializedMap.containsKey("v") ? ((Number)deserializedMap.get("v")).intValue() : -1; - if (itemStackVersion >= 0) { - @SuppressWarnings("deprecation") - int currentDataVersion = Bukkit.getUnsafe().getDataVersion(); - if (itemStackVersion > currentDataVersion) { - /* The itemStack we are deserializing is from a newer MC version, so Bukkit will refuse it. - * We decide to ignore the provided version and consider that the received item stack is from current - * version. We let Bukkit handles the deserialization with the data it can interpret, throwing an error - * only if it can't. - */ - deserializedMap.put("v", currentDataVersion); - return ItemStack.deserialize(deserializedMap); - } + if (!(json instanceof JsonObject jsonObj)) + throw new JsonParseException("Unable to deserialize a ConfigurationSerializable from the provided json structure."); + if (jsonObj.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) + return context.deserialize(jsonObj, ConfigurationSerializable.class); + + + if (jsonObj.has("meta") + && jsonObj.get("meta") instanceof JsonObject metaJson + && !metaJson.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) { + // item meta was serialized using GSON reflection serializer, instead of proper serialization using + // ConfigurationSerializable interface. So we try to deserialize it the same way. + + Map map = context.deserialize(jsonObj, MAP_STR_OBJ_TYPE.getType()); + fixDeserializationVersion(map); + map.remove("meta"); + ItemStack is = ItemStack.deserialize(map); + + Class metaClass = is.getItemMeta().getClass(); + ItemMeta meta = vanillaGson.fromJson(jsonObj.get("meta"), metaClass); + is.setItemMeta(meta); + return is; } - return ItemStack.deserialize(deserializedMap); + // deserialize using ConfigurationSerializableAdapter + jsonObj.addProperty(ConfigurationSerialization.SERIALIZED_TYPE_KEY, + ConfigurationSerialization.getAlias(ItemStack.class)); + return context.deserialize(jsonObj, ConfigurationSerializable.class); } @Override public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) { return context.serialize(src.serialize(), MAP_STR_OBJ_TYPE.getType()); } + + + + /* package */ static void fixDeserializationVersion(Map deserializedMap) { + if (!deserializedMap.containsKey("v")) + return; + int itemStackVersion = ((Number)deserializedMap.get("v")).intValue(); + if (itemStackVersion >= 0) { + @SuppressWarnings("deprecation") + int currentDataVersion = Bukkit.getUnsafe().getDataVersion(); + if (itemStackVersion > currentDataVersion) { + /* Here, the itemStack we are deserializing is from a newer MC version, so Bukkit will refuse it. + * We decide to ignore the provided version and consider that the received item stack is from current + * version. We let Bukkit handles the deserialization with the data it can interpret, throwing an error + * only if it can't. + */ + deserializedMap.put("v", currentDataVersion); + } + } + } } diff --git a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/PaperJson.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/PaperJson.java index d1f1969..1d5d07a 100644 --- a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/PaperJson.java +++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/json/PaperJson.java @@ -12,5 +12,6 @@ public class PaperJson { */ public static void init() { Json.registerTypeAdapterFactory(ItemStackAdapter.FACTORY); + Json.registerTypeAdapterFactory(ConfigurationSerializableAdapter.FACTORY); } }