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}