001package com.jeff_media.jsonconfigurationserialization; 002 003import com.google.gson.*; 004import com.google.gson.reflect.TypeToken; 005import org.bukkit.configuration.serialization.ConfigurationSerializable; 006import org.bukkit.configuration.serialization.ConfigurationSerialization; 007 008import java.lang.reflect.Type; 009import java.util.HashMap; 010import java.util.Map; 011 012/** 013 * A {@link JsonSerializer} and {@link JsonDeserializer} for {@link ConfigurationSerializable}s to be used with {@link GsonBuilder#registerTypeHierarchyAdapter(Class, Object)} 014 */ 015public final class ConfigurationSerializableTypeHierarchyAdapter implements JsonSerializer<ConfigurationSerializable>, JsonDeserializer<ConfigurationSerializable> { 016 017 private static final String SERIALIZED_TYPE_KEY = ConfigurationSerialization.SERIALIZED_TYPE_KEY; 018 static final TypeToken<Map<String, Object>> MAP_TYPE = new TypeToken<Map<String, Object>>() { 019 }; 020 021 static ConfigurationSerializable deserializeFromMap(Map<String, Object> map) throws IllegalArgumentException { 022 deserializeInner(map); 023 return ConfigurationSerialization.deserializeObject(map); 024 } 025 026 static Map<String, Object> serializeToMap(ConfigurationSerializable serializable) { 027 Map<String, Object> map = new HashMap<>(serializable.serialize()); 028 map.put(SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(serializable.getClass())); 029 serializeInner(map); 030 return map; 031 } 032 033 private static void serializeInner(Map<String, Object> map) { 034 for (Map.Entry<String, Object> entry : map.entrySet()) { 035 if (entry.getValue() instanceof ConfigurationSerializable) { 036 Map<String, Object> innerMap = new HashMap<>(((ConfigurationSerializable) entry.getValue()).serialize()); 037 innerMap.put(SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias((Class<? extends ConfigurationSerializable>) entry.getValue().getClass())); 038 serializeInner(innerMap); 039 entry.setValue(innerMap); 040 } 041 } 042 } 043 044 private static void deserializeInner(Map<String, Object> map) { 045 for (Map.Entry<String, Object> entry : map.entrySet()) { 046 047 Object raw = entry.getValue(); 048 049 if (raw instanceof Map) { 050 Map<String, Object> innerMap = (Map<String, Object>) raw; 051 deserializeInner(innerMap); 052 if (innerMap.containsKey(SERIALIZED_TYPE_KEY)) { 053 String alias = (String) innerMap.get(SERIALIZED_TYPE_KEY); 054 Class<? extends ConfigurationSerializable> clazz = ConfigurationSerialization.getClassByAlias(alias); 055 if (clazz != null) { 056 ConfigurationSerializable serializable = ConfigurationSerialization.deserializeObject(innerMap, clazz); 057 entry.setValue(serializable); 058 } else { 059 throw new IllegalArgumentException("Could not find class by alias: " + alias); 060 } 061 } 062 } else { 063 // Gson by default deserializes all numbers as doubles, so we need to convert them back to ints if possible. 064 // Otherwise, certain deserialization methods will not work, for example CraftMetaItem#buildEnchantments does an instanceof Integer check. 065 // And no, we cannot use a custom ToNumberStrategy because Gson does not allow us to specify a ToNumberStrategy for a specific type. 066 if(raw instanceof Number) { 067 Number number = (Number) raw; 068 entry.setValue(narrowNumberType(number)); 069 } 070 } 071 } 072 } 073 074 075 private static Number narrowNumberType(Number number) { 076 long asLong = number.longValue(); 077 078 if(number.doubleValue() == (double) asLong) { 079 if(asLong > Integer.MAX_VALUE || asLong < Integer.MIN_VALUE) { 080 return asLong; 081 } else { 082 return number.intValue(); 083 } 084 } 085 086 return number; 087 } 088 089 @Override 090 public JsonElement serialize(ConfigurationSerializable configurationSerializable, Type type, JsonSerializationContext jsonSerializationContext) { 091 return jsonSerializationContext.serialize(serializeToMap(configurationSerializable), MAP_TYPE.getType()); 092 } 093 094 @Override 095 public ConfigurationSerializable deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { 096 return deserializeFromMap(jsonDeserializationContext.deserialize(jsonElement, MAP_TYPE.getType())); 097 } 098}