Proper serialization of ItemStack and other Serializable stuff in Bukkit API

This commit is contained in:
Marc Baloup 2023-10-07 19:42:26 +02:00
parent da1ee9d882
commit 8b6fe63df1
4 changed files with 134 additions and 15 deletions

View File

@ -52,6 +52,7 @@ public class Json {
private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) { private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) {
GsonBuilder base = new GsonBuilder() GsonBuilder base = new GsonBuilder()
.registerTypeAdapterFactory(new CustomAdapterFactory()) .registerTypeAdapterFactory(new CustomAdapterFactory())
.disableHtmlEscaping()
.setLenient(); .setLenient();
return builderModifier.apply(base).create(); return builderModifier.apply(base).create();
} }

View File

@ -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 Bukkits Yaml API to convert the objects from/to json.
*/
/* package */ class ConfigurationSerializableAdapter implements JsonSerializer<ConfigurationSerializable>, JsonDeserializer<ConfigurationSerializable> {
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(ConfigurationSerializable.class, new ConfigurationSerializableAdapter());
private static final TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
private boolean isItemStack(Map<String, Object> 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<String, Object> 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<String, Object> 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));
}
}

View File

@ -1,8 +1,11 @@
package fr.pandacube.lib.paper.json; 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.JsonDeserializationContext;
import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer; 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.internal.bind.TreeTypeAdapter;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.bukkit.Bukkit; 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.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
@ -21,30 +27,62 @@ import java.util.Map;
private static final TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { }; private static final TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
/** Gson instance with no custom type adapter */
private static final Gson vanillaGson = new GsonBuilder().setLenient().create();
@Override @Override
public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Map<String, Object> deserializedMap = context.deserialize(json, MAP_STR_OBJ_TYPE.getType()); if (!(json instanceof JsonObject jsonObj))
int itemStackVersion = deserializedMap.containsKey("v") ? ((Number)deserializedMap.get("v")).intValue() : -1; throw new JsonParseException("Unable to deserialize a ConfigurationSerializable from the provided json structure.");
if (itemStackVersion >= 0) { if (jsonObj.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY))
@SuppressWarnings("deprecation") return context.deserialize(jsonObj, ConfigurationSerializable.class);
int currentDataVersion = Bukkit.getUnsafe().getDataVersion();
if (itemStackVersion > currentDataVersion) {
/* The itemStack we are deserializing is from a newer MC version, so Bukkit will refuse it. if (jsonObj.has("meta")
* We decide to ignore the provided version and consider that the received item stack is from current && jsonObj.get("meta") instanceof JsonObject metaJson
* version. We let Bukkit handles the deserialization with the data it can interpret, throwing an error && !metaJson.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
* only if it can't. // item meta was serialized using GSON reflection serializer, instead of proper serialization using
*/ // ConfigurationSerializable interface. So we try to deserialize it the same way.
deserializedMap.put("v", currentDataVersion);
return ItemStack.deserialize(deserializedMap); Map<String, Object> map = context.deserialize(jsonObj, MAP_STR_OBJ_TYPE.getType());
} fixDeserializationVersion(map);
map.remove("meta");
ItemStack is = ItemStack.deserialize(map);
Class<? extends ItemMeta> 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 @Override
public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) { public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.serialize(), MAP_STR_OBJ_TYPE.getType()); return context.serialize(src.serialize(), MAP_STR_OBJ_TYPE.getType());
} }
/* package */ static void fixDeserializationVersion(Map<String, Object> 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);
}
}
}
} }

View File

@ -12,5 +12,6 @@ public class PaperJson {
*/ */
public static void init() { public static void init() {
Json.registerTypeAdapterFactory(ItemStackAdapter.FACTORY); Json.registerTypeAdapterFactory(ItemStackAdapter.FACTORY);
Json.registerTypeAdapterFactory(ConfigurationSerializableAdapter.FACTORY);
} }
} }