001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.configuration;
020
021import com.plotsquared.core.configuration.Settings.Enabled_Components;
022import com.plotsquared.core.configuration.file.YamlConfiguration;
023import com.plotsquared.core.util.StringMan;
024import org.apache.logging.log4j.LogManager;
025import org.apache.logging.log4j.Logger;
026
027import java.io.File;
028import java.io.PrintWriter;
029import java.lang.annotation.ElementType;
030import java.lang.annotation.Retention;
031import java.lang.annotation.RetentionPolicy;
032import java.lang.annotation.Target;
033import java.lang.invoke.MethodHandles;
034import java.lang.reflect.Field;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040
041public class Config {
042
043    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + Config.class.getSimpleName());
044
045    /**
046     * Set the value of a specific node<br>
047     * Probably throws some error if you supply non existing keys or invalid values
048     *
049     * @param key   config node
050     * @param value value
051     * @param root  configuration class
052     */
053    public static void set(String key, Object value, Class<? extends Config> root) {
054        String[] split = key.split("\\.");
055        Object instance = getInstance(split, root);
056        if (instance != null) {
057            Field field = getField(split, instance);
058            if (field != null) {
059                try {
060                    if (field.getAnnotation(Final.class) != null) {
061                        return;
062                    }
063                    if (field.getType() == String.class && !(value instanceof String)) {
064                        value = value + "";
065                    }
066                    field.set(instance, value);
067                    return;
068                } catch (final Throwable e) {
069                    LOGGER.error("Invalid configuration value '{}: {}' in {}", key, value, root.getSimpleName());
070                    e.printStackTrace();
071                }
072            }
073        }
074        LOGGER.error("Failed to set config option '{}: {}' | {}", key, value, instance);
075    }
076
077    public static boolean load(File file, Class<? extends Config> root) {
078        if (!file.exists()) {
079            return false;
080        }
081        YamlConfiguration yml = YamlConfiguration.loadConfiguration(file);
082        for (String key : yml.getKeys(true)) {
083            Object value = yml.get(key);
084            if (value instanceof MemorySection) {
085                continue;
086            }
087            set(key, value, root);
088        }
089        return true;
090    }
091
092    /**
093     * Set all values in the file (load first to avoid overwriting)
094     *
095     * @param file file
096     * @param root configuration file class
097     */
098    public static void save(File file, Class<? extends Config> root) {
099        try {
100            if (!file.exists()) {
101                file.getParentFile().mkdirs();
102                file.createNewFile();
103            }
104            try (PrintWriter writer = new PrintWriter(file)) {
105                Object instance = root.getDeclaredConstructor().newInstance();
106                save(writer, root, instance, 0);
107            }
108        } catch (Throwable e) {
109            e.printStackTrace();
110        }
111    }
112
113    /**
114     * Get the static fields in a section.
115     *
116     * @param clazz config section
117     * @return map or string against object of static fields
118     */
119    public static Map<String, Object> getFields(Class<Enabled_Components> clazz) {
120        HashMap<String, Object> map = new HashMap<>();
121        for (Field field : clazz.getFields()) {
122            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
123                try {
124                    map.put(toNodeName(field.getName()), field.get(null));
125                } catch (IllegalAccessException e) {
126                    e.printStackTrace();
127                }
128            }
129        }
130        return map;
131    }
132
133    private static String toYamlString(Object value, String spacing) {
134        if (value instanceof List) {
135            Collection<?> listValue = (Collection<?>) value;
136            if (listValue.isEmpty()) {
137                return "[]";
138            }
139            StringBuilder m = new StringBuilder();
140            for (Object obj : listValue) {
141                m.append(System.lineSeparator()).append(spacing).append("- ").append(toYamlString(obj, spacing));
142            }
143            return m.toString();
144        }
145        if (value instanceof String stringValue) {
146            if (stringValue.isEmpty()) {
147                return "''";
148            }
149            return "\"" + stringValue + "\"";
150        }
151        return value != null ? value.toString() : "null";
152    }
153
154    @SuppressWarnings({"unchecked", "rawtypes"})
155    private static void save(PrintWriter writer, Class<?> clazz, Object instance, int indent) {
156        try {
157            String lineSeparator = System.lineSeparator();
158            String spacing = StringMan.repeat(" ", indent);
159            for (Field field : clazz.getFields()) {
160                if (field.getAnnotation(Ignore.class) != null) {
161                    continue;
162                }
163                Comment comment = field.getAnnotation(Comment.class);
164                if (comment != null) {
165                    for (String commentLine : comment.value()) {
166                        writer.write(spacing + "# " + commentLine + lineSeparator);
167                    }
168                }
169                Create create = field.getAnnotation(Create.class);
170                if (create != null) {
171                    Object value = field.get(instance);
172                    if (value == null && field.getType() != ConfigBlock.class) {
173                        setAccessible(field);
174                        Class<?>[] classes = clazz.getDeclaredClasses();
175                        for (Class<?> current : classes) {
176                            if (StringMan.isEqual(current.getSimpleName(), field.getName())) {
177                                field.set(instance, current.getDeclaredConstructor().newInstance());
178                                break;
179                            }
180                        }
181                    }
182                } else {
183                    writer.write(spacing + toNodeName(field.getName() + ": ") + toYamlString(
184                            field.get(instance), spacing) + lineSeparator);
185                }
186            }
187            for (Class<?> current : clazz.getClasses()) {
188                if (current.isInterface() || current.getAnnotation(Ignore.class) != null) {
189                    continue;
190                }
191                if (indent == 0) {
192                    writer.write(lineSeparator);
193                }
194                Comment comment = current.getAnnotation(Comment.class);
195                if (comment != null) {
196                    for (String commentLine : comment.value()) {
197                        writer.write(spacing + "# " + commentLine + lineSeparator);
198                    }
199                }
200                writer.write(spacing + toNodeName(current.getSimpleName()) + ":" + lineSeparator);
201                BlockName blockNames = current.getAnnotation(BlockName.class);
202                if (blockNames != null) {
203                    Field instanceField =
204                            clazz.getDeclaredField(toFieldName(current.getSimpleName()));
205                    setAccessible(instanceField);
206                    ConfigBlock value = (ConfigBlock<?>) instanceField.get(instance);
207                    if (value == null) {
208                        value = new ConfigBlock();
209                        instanceField.set(instance, value);
210                        for (String blockName : blockNames.value()) {
211                            value.put(blockName, current.getDeclaredConstructor().newInstance());
212                        }
213                    }
214                    // Save each instance
215                    for (Map.Entry<String, Object> entry : ((Map<String, Object>) value.getRaw())
216                            .entrySet()) {
217                        String key = entry.getKey();
218                        writer.write(spacing + "  " + toNodeName(key) + ":" + lineSeparator);
219                        save(writer, current, entry.getValue(), indent + 4);
220                    }
221                } else {
222                    save(writer, current, current.getDeclaredConstructor().newInstance(), indent + 2);
223                }
224            }
225        } catch (Throwable e) {
226            e.printStackTrace();
227        }
228    }
229
230    /**
231     * Get the field for a specific config node and instance<br>
232     * Note: As expiry can have multiple blocks there will be multiple instances
233     *
234     * @param split    the node (split by period)
235     * @param instance the instance
236     */
237    private static Field getField(String[] split, Object instance) {
238        try {
239            Field field = instance.getClass().getField(toFieldName(split[split.length - 1]));
240            setAccessible(field);
241            return field;
242        } catch (final Throwable e) {
243            LOGGER.error("Invalid config field: {} for {}. It's likely you are in the process of updating from an older major " +
244                            "release of PlotSquared. The entries named can be removed safely from the settings.yml. They are " +
245                            "likely no longer in use, moved to a different location or have been merged with other " +
246                            "configuration options. Check the changelog for more information.",
247                    StringMan.join(split, "."), toNodeName(instance.getClass().getSimpleName())
248            );
249            e.printStackTrace();
250            return null;
251        }
252    }
253
254    /**
255     * Get the instance for a specific config node.
256     *
257     * @param split the node (split by period)
258     * @param root
259     * @return The instance or null
260     */
261    @SuppressWarnings({"unchecked", "rawtypes"})
262    private static Object getInstance(String[] split, Class<?> root) {
263        try {
264            Class<?> clazz = root == null ? MethodHandles.lookup().lookupClass() : root;
265            Object instance = clazz.getDeclaredConstructor().newInstance();
266            while (split.length > 0) {
267                if (split.length == 1) {
268                    return instance;
269                }
270                Class<?> found = null;
271                Class<?>[] classes = clazz.getDeclaredClasses();
272                for (Class<?> current : classes) {
273                    if (current.getSimpleName().equalsIgnoreCase(toFieldName(split[0]))) {
274                        found = current;
275                        break;
276                    }
277                }
278                try {
279                    Field instanceField = clazz.getDeclaredField(toFieldName(split[0]));
280                    setAccessible(instanceField);
281                    if (instanceField.getType() != ConfigBlock.class) {
282                        Object value = instanceField.get(instance);
283                        if (value == null) {
284                            value = found.getDeclaredConstructor().newInstance();
285                            instanceField.set(instance, value);
286                        }
287                        clazz = found;
288                        instance = value;
289                        split = Arrays.copyOfRange(split, 1, split.length);
290                        continue;
291                    }
292                    ConfigBlock value = (ConfigBlock<?>) instanceField.get(instance);
293                    if (value == null) {
294                        value = new ConfigBlock();
295                        instanceField.set(instance, value);
296                    }
297                    instance = value.get(split[1]);
298                    if (instance == null) {
299                        instance = found.getDeclaredConstructor().newInstance();
300                        value.put(split[1], instance);
301                    }
302                    clazz = found;
303                    split = Arrays.copyOfRange(split, 2, split.length);
304                    continue;
305                } catch (NoSuchFieldException ignore) {
306                }
307                if (found != null) {
308                    split = Arrays.copyOfRange(split, 1, split.length);
309                    clazz = found;
310                    instance = clazz.getDeclaredConstructor().newInstance();
311                    continue;
312                }
313                return null;
314            }
315        } catch (Throwable e) {
316            e.printStackTrace();
317        }
318        return null;
319    }
320
321    /**
322     * Translate a node to a java field name.
323     *
324     * @param node
325     * @return
326     */
327    private static String toFieldName(String node) {
328        return node.toUpperCase().replaceAll("-", "_");
329    }
330
331    /**
332     * Translate a field to a config node.
333     *
334     * @param field
335     * @return
336     */
337    private static String toNodeName(String field) {
338        return field.toLowerCase().replace("_", "-");
339    }
340
341    /**
342     * Set some field to be accessible.
343     *
344     * @param field
345     */
346    private static void setAccessible(Field field) {
347        field.setAccessible(true);
348    }
349
350    /**
351     * Indicates that a field should be instantiated / created.
352     */
353    @Retention(RetentionPolicy.RUNTIME)
354    @Target({ElementType.FIELD})
355    public @interface Create {
356
357    }
358
359
360    /**
361     * Indicates that a field cannot be modified.
362     */
363    @Retention(RetentionPolicy.RUNTIME)
364    @Target({ElementType.FIELD})
365    public @interface Final {
366
367    }
368
369
370    /**
371     * Creates a comment.
372     */
373    @Retention(RetentionPolicy.RUNTIME)
374    @Target({ElementType.FIELD, ElementType.TYPE})
375    public @interface Comment {
376
377        String[] value();
378
379    }
380
381
382    /**
383     * The names of any default blocks.
384     */
385    @Retention(RetentionPolicy.RUNTIME)
386    @Target({ElementType.FIELD, ElementType.TYPE})
387    public @interface BlockName {
388
389        String[] value();
390
391    }
392
393
394    /**
395     * Any field or class with is not part of the config.
396     */
397    @Retention(RetentionPolicy.RUNTIME)
398    @Target({ElementType.FIELD, ElementType.TYPE})
399    public @interface Ignore {
400
401    }
402
403
404    @Ignore // This is not part of the config
405    public static class ConfigBlock<T> {
406
407        private final HashMap<String, T> INSTANCES = new HashMap<>();
408
409        public T get(String key) {
410            return INSTANCES.get(key);
411        }
412
413        public void put(String key, T value) {
414            INSTANCES.put(key, value);
415        }
416
417        public Collection<T> getInstances() {
418            return INSTANCES.values();
419        }
420
421        private Map<String, T> getRaw() {
422            return INSTANCES;
423        }
424
425    }
426
427}