aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
blob: a0f7ee40d572c867e36e0477121db93c50d3d4b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
package dev.isxander.yacl3.config;

import com.google.gson.*;
import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer;
import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
import dev.isxander.yacl3.impl.utils.YACLConstants;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.world.item.Item;

import java.awt.*;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.function.UnaryOperator;

/**
 * Uses GSON to serialize and deserialize config data from JSON to a file.
 * <p>
 * Only fields annotated with {@link ConfigEntry} are included in the JSON.
 * {@link Component}, {@link Style} and {@link Color} have default type adapters, so there is no need to provide them in your GSON instance.
 * GSON is automatically configured to format fields as {@code lower_camel_case}.
 *
 * @param <T> config data type
 * @deprecated upgrade to config v2 {@link dev.isxander.yacl3.config.v2.api.ConfigClassHandler} with {@link dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder}
 *          <pre>
 * {@code
 * public class MyConfig {
 *     public static ConfigClassHandler<MyConfig> HANDLER = ConfigClassHandler.createBuilder(MyConfig.class)
 *             .id(new ResourceLocation("modid", "config"))
 *             .serializer(config -> GsonConfigSerializerBuilder.create(config)
 *                     .setPath(FabricLoader.getInstance().getConfigDir().resolve("my_mod.json")
 *                     .build())
 *             .build();
 *
 *     @SerialEntry public boolean myBoolean = true;
 * }
 * }
 * </pre>
 */
@Deprecated
public class GsonConfigInstance<T> extends ConfigInstance<T> {
    private final Gson gson;
    private final Path path;

    @Deprecated
    public GsonConfigInstance(Class<T> configClass, Path path) {
        this(configClass, path, new GsonBuilder());
    }

    @Deprecated
    public GsonConfigInstance(Class<T> configClass, Path path, Gson gson) {
        this(configClass, path, gson.newBuilder());
    }

    @Deprecated
    public GsonConfigInstance(Class<T> configClass, Path path, UnaryOperator<GsonBuilder> builder) {
        this(configClass, path, builder.apply(new GsonBuilder()));
    }

    @Deprecated
    public GsonConfigInstance(Class<T> configClass, Path path, GsonBuilder builder) {
        super(configClass);
        this.path = path;
        this.gson = builder
                .setExclusionStrategies(new ConfigExclusionStrategy())
                /*? if >1.20.4 {*/
                .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY))
                /*?} elif =1.20.4 {*/
                /*.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
                *//*?} else {*/
                /*.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
                *//*?}*/
                .registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/)
                .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
                .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter())
                .serializeNulls()
                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                .create();
    }

    private GsonConfigInstance(Class<T> configClass, Path path, Gson gson, boolean fromBuilder) {
        super(configClass);
        this.path = path;
        this.gson = gson;
    }

    @Override
    public void save() {
        try {
            YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName());
            Files.writeString(path, gson.toJson(getConfig()), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void load() {
        try {
            if (Files.notExists(path)) {
                save();
                return;
            }

            YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName());
            setConfig(gson.fromJson(Files.readString(path), getConfigClass()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Path getPath() {
        return this.path;
    }

    private static class ConfigExclusionStrategy implements ExclusionStrategy {
        @Override
        public boolean shouldSkipField(FieldAttributes fieldAttributes) {
            return fieldAttributes.getAnnotation(ConfigEntry.class) == null;
        }

        @Override
        public boolean shouldSkipClass(Class<?> aClass) {
            return false;
        }
    }

    public static class ColorTypeAdapter implements JsonSerializer<Color>, JsonDeserializer<Color> {
        @Override
        public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
            return new Color(jsonElement.getAsInt(), true);
        }

        @Override
        public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) {
            return new JsonPrimitive(color.getRGB());
        }
    }
    public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> {
        @Override
        public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
            return ItemRegistryHelper.getItemFromName(jsonElement.getAsString());
        }

        @Override
        public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) {
            return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString());
        }
    }

    /**
     * Creates a builder for a GSON config instance.
     * @param configClass the config class
     * @return a new builder
     * @param <T> the config type
     */
    public static <T> Builder<T> createBuilder(Class<T> configClass) {
        return new Builder<>(configClass);
    }

    public static class Builder<T> {
        private final Class<T> configClass;
        private Path path;
        private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder
                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                .serializeNulls()
                /*? if >1.20.4 {*/
                .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY))
                /*?} elif =1.20.4 {*/
                /*.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
                *//*?} else {*/
                /*.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
                *//*?}*/
                .registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/)
                .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
                .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter());

        private Builder(Class<T> configClass) {
            this.configClass = configClass;
        }

        /**
         * Sets the file path to save and load the config from.
         */
        public Builder<T> setPath(Path path) {
            this.path = path;
            return this;
        }

        /**
         * Sets the GSON instance to use. Overrides all YACL defaults such as:
         * <ul>
         *     <li>lower_camel_case field naming policy</li>
         *     <li>null serialization</li>
         *     <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
         * </ul>
         * Still respects the exclusion strategy to only serialize {@link ConfigEntry}
         * but these can be added to with setExclusionStrategies.
         *
         * @param gsonBuilder gson builder to use
         */
        public Builder<T> overrideGsonBuilder(GsonBuilder gsonBuilder) {
            this.gsonBuilder = builder -> gsonBuilder;
            return this;
        }

        /**
         * Sets the GSON instance to use. Overrides all YACL defaults such as:
         * <ul>
         *     <li>lower_camel_case field naming policy</li>
         *     <li>null serialization</li>
         *     <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
         * </ul>
         * Still respects the exclusion strategy to only serialize {@link ConfigEntry}
         * but these can be added to with setExclusionStrategies.
         *
         * @param gson gson instance to be converted to a builder
         */
        public Builder<T> overrideGsonBuilder(Gson gson) {
            return this.overrideGsonBuilder(gson.newBuilder());
        }

        /**
         * Appends extra configuration to a GSON builder.
         * This is the intended way to add functionality to the GSON instance.
         * <p>
         * By default, YACL sets the GSON with the following options:
         * <ul>
         *     <li>lower_camel_case field naming policy</li>
         *     <li>null serialization</li>
         *     <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
         * </ul>
         *
         * @param gsonBuilder the function to apply to the builder
         */
        public Builder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder) {
            UnaryOperator<GsonBuilder> prev = this.gsonBuilder;
            this.gsonBuilder = builder -> gsonBuilder.apply(prev.apply(builder));
            return this;
        }

        /**
         * Builds the config instance.
         * @return the built config instance
         */
        public GsonConfigInstance<T> build() {
            UnaryOperator<GsonBuilder> gsonBuilder = builder -> this.gsonBuilder.apply(builder)
                    .addSerializationExclusionStrategy(new ConfigExclusionStrategy())
                    .addDeserializationExclusionStrategy(new ConfigExclusionStrategy());

            return new GsonConfigInstance<>(configClass, path, gsonBuilder.apply(new GsonBuilder()).create(), true);
        }
    }
}