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     * Get the value for a node<br>
047     * Probably throws some error if you try to get a non existent key
048     *
049     * @param key  configuration key
050     * @param root configuration class
051     * @param <T>  value type
052     * @return value
053     * @deprecated Unused internally. Scheduled for removal in next major release.
054     */
055    @Deprecated(forRemoval = true, since = "6.11.1")
056    @SuppressWarnings("unchecked")
057    public static <T> T get(String key, Class<?> root) {
058        String[] split = key.split("\\.");
059        Object instance = getInstance(split, root);
060        if (instance != null) {
061            Field field = getField(split, instance);
062            if (field != null) {
063                try {
064                    return (T) field.get(instance);
065                } catch (IllegalAccessException e) {
066                    e.printStackTrace();
067                }
068            }
069        }
070        return null;
071    }
072
073    /**
074     * Set the value of a specific node<br>
075     * Probably throws some error if you supply non existing keys or invalid values
076     *
077     * @param key   config node
078     * @param value value
079     * @param root  configuration class
080     */
081    public static void set(String key, Object value, Class<? extends Config> root) {
082        String[] split = key.split("\\.");
083        Object instance = getInstance(split, root);
084        if (instance != null) {
085            Field field = getField(split, instance);
086            if (field != null) {
087                try {
088                    if (field.getAnnotation(Final.class) != null) {
089                        return;
090                    }
091                    if (field.getType() == String.class && !(value instanceof String)) {
092                        value = value + "";
093                    }
094                    field.set(instance, value);
095                    return;
096                } catch (final Throwable e) {
097                    LOGGER.error("Invalid configuration value '{}: {}' in {}", key, value, root.getSimpleName());
098                    e.printStackTrace();
099                }
100            }
101        }
102        LOGGER.error("Failed to set config option '{}: {}' | {}", key, value, instance);
103    }
104
105    public static boolean load(File file, Class<? extends Config> root) {
106        if (!file.exists()) {
107            return false;
108        }
109        YamlConfiguration yml = YamlConfiguration.loadConfiguration(file);
110        for (String key : yml.getKeys(true)) {
111            Object value = yml.get(key);
112            if (value instanceof MemorySection) {
113                continue;
114            }
115            set(key, value, root);
116        }
117        return true;
118    }
119
120    /**
121     * Set all values in the file (load first to avoid overwriting)
122     *
123     * @param file file
124     * @param root configuration file class
125     */
126    public static void save(File file, Class<? extends Config> root) {
127        try {
128            if (!file.exists()) {
129                file.getParentFile().mkdirs();
130                file.createNewFile();
131            }
132            try (PrintWriter writer = new PrintWriter(file)) {
133                Object instance = root.getDeclaredConstructor().newInstance();
134                save(writer, root, instance, 0);
135            }
136        } catch (Throwable e) {
137            e.printStackTrace();
138        }
139    }
140
141    /**
142     * Get the static fields in a section.
143     *
144     * @param clazz config section
145     * @return map or string against object of static fields
146     */
147    public static Map<String, Object> getFields(Class<Enabled_Components> clazz) {
148        HashMap<String, Object> map = new HashMap<>();
149        for (Field field : clazz.getFields()) {
150            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
151                try {
152                    map.put(toNodeName(field.getName()), field.get(null));
153                } catch (IllegalAccessException e) {
154                    e.printStackTrace();
155                }
156            }
157        }
158        return map;
159    }
160
161    private static String toYamlString(Object value, String spacing) {
162        if (value instanceof List) {
163            Collection<?> listValue = (Collection<?>) value;
164            if (listValue.isEmpty()) {
165                return "[]";
166            }
167            StringBuilder m = new StringBuilder();
168            for (Object obj : listValue) {
169                m.append(System.lineSeparator()).append(spacing).append("- ").append(toYamlString(obj, spacing));
170            }
171            return m.toString();
172        }
173        if (value instanceof String stringValue) {
174            if (stringValue.isEmpty()) {
175                return "''";
176            }
177            return "\"" + stringValue + "\"";
178        }
179        return value != null ? value.toString() : "null";
180    }
181
182    @SuppressWarnings({"unchecked", "rawtypes"})
183    private static void save(PrintWriter writer, Class<?> clazz, Object instance, int indent) {
184        try {
185            String lineSeparator = System.lineSeparator();
186            String spacing = StringMan.repeat(" ", indent);
187            for (Field field : clazz.getFields()) {
188                if (field.getAnnotation(Ignore.class) != null) {
189                    continue;
190                }
191                Comment comment = field.getAnnotation(Comment.class);
192                if (comment != null) {
193                    for (String commentLine : comment.value()) {
194                        writer.write(spacing + "# " + commentLine + lineSeparator);
195                    }
196                }
197                Create create = field.getAnnotation(Create.class);
198                if (create != null) {
199                    Object value = field.get(instance);
200                    if (value == null && field.getType() != ConfigBlock.class) {
201                        setAccessible(field);
202                        Class<?>[] classes = clazz.getDeclaredClasses();
203                        for (Class<?> current : classes) {
204                            if (StringMan.isEqual(current.getSimpleName(), field.getName())) {
205                                field.set(instance, current.getDeclaredConstructor().newInstance());
206                                break;
207                            }
208                        }
209                    }
210                } else {
211                    writer.write(spacing + toNodeName(field.getName() + ": ") + toYamlString(
212                            field.get(instance), spacing) + lineSeparator);
213                }
214            }
215            for (Class<?> current : clazz.getClasses()) {
216                if (current.isInterface() || current.getAnnotation(Ignore.class) != null) {
217                    continue;
218                }
219                if (indent == 0) {
220                    writer.write(lineSeparator);
221                }
222                Comment comment = current.getAnnotation(Comment.class);
223                if (comment != null) {
224                    for (String commentLine : comment.value()) {
225                        writer.write(spacing + "# " + commentLine + lineSeparator);
226                    }
227                }
228                writer.write(spacing + toNodeName(current.getSimpleName()) + ":" + lineSeparator);
229                BlockName blockNames = current.getAnnotation(BlockName.class);
230                if (blockNames != null) {
231                    Field instanceField =
232                            clazz.getDeclaredField(toFieldName(current.getSimpleName()));
233                    setAccessible(instanceField);
234                    ConfigBlock value = (ConfigBlock<?>) instanceField.get(instance);
235                    if (value == null) {
236                        value = new ConfigBlock();
237                        instanceField.set(instance, value);
238                        for (String blockName : blockNames.value()) {
239                            value.put(blockName, current.getDeclaredConstructor().newInstance());
240                        }
241                    }
242                    // Save each instance
243                    for (Map.Entry<String, Object> entry : ((Map<String, Object>) value.getRaw())
244                            .entrySet()) {
245                        String key = entry.getKey();
246                        writer.write(spacing + "  " + toNodeName(key) + ":" + lineSeparator);
247                        save(writer, current, entry.getValue(), indent + 4);
248                    }
249                } else {
250                    save(writer, current, current.getDeclaredConstructor().newInstance(), indent + 2);
251                }
252            }
253        } catch (Throwable e) {
254            e.printStackTrace();
255        }
256    }
257
258    /**
259     * Get the field for a specific config node and instance<br>
260     * Note: As expiry can have multiple blocks there will be multiple instances
261     *
262     * @param split    the node (split by period)
263     * @param instance the instance
264     * @return
265     */
266    private static Field getField(String[] split, Object instance) {
267        try {
268            Field field = instance.getClass().getField(toFieldName(split[split.length - 1]));
269            setAccessible(field);
270            return field;
271        } catch (final Throwable e) {
272            LOGGER.error("Invalid config field: {} for {}. It's likely you are in the process of updating from an older major " +
273                            "release of PlotSquared. The entries named can be removed safely from the settings.yml. They are " +
274                            "likely no longer in use, moved to a different location or have been merged with other " +
275                            "configuration options. Check the changelog for more information.",
276                    StringMan.join(split, "."), toNodeName(instance.getClass().getSimpleName())
277            );
278            e.printStackTrace();
279            return null;
280        }
281    }
282
283    /**
284     * Get the instance for a specific config node.
285     *
286     * @param split the node (split by period)
287     * @param root
288     * @return The instance or null
289     */
290    @SuppressWarnings({"unchecked", "rawtypes"})
291    private static Object getInstance(String[] split, Class<?> root) {
292        try {
293            Class<?> clazz = root == null ? MethodHandles.lookup().lookupClass() : root;
294            Object instance = clazz.getDeclaredConstructor().newInstance();
295            while (split.length > 0) {
296                if (split.length == 1) {
297                    return instance;
298                }
299                Class<?> found = null;
300                Class<?>[] classes = clazz.getDeclaredClasses();
301                for (Class<?> current : classes) {
302                    if (current.getSimpleName().equalsIgnoreCase(toFieldName(split[0]))) {
303                        found = current;
304                        break;
305                    }
306                }
307                try {
308                    Field instanceField = clazz.getDeclaredField(toFieldName(split[0]));
309                    setAccessible(instanceField);
310                    if (instanceField.getType() != ConfigBlock.class) {
311                        Object value = instanceField.get(instance);
312                        if (value == null) {
313                            value = found.getDeclaredConstructor().newInstance();
314                            instanceField.set(instance, value);
315                        }
316                        clazz = found;
317                        instance = value;
318                        split = Arrays.copyOfRange(split, 1, split.length);
319                        continue;
320                    }
321                    ConfigBlock value = (ConfigBlock<?>) instanceField.get(instance);
322                    if (value == null) {
323                        value = new ConfigBlock();
324                        instanceField.set(instance, value);
325                    }
326                    instance = value.get(split[1]);
327                    if (instance == null) {
328                        instance = found.getDeclaredConstructor().newInstance();
329                        value.put(split[1], instance);
330                    }
331                    clazz = found;
332                    split = Arrays.copyOfRange(split, 2, split.length);
333                    continue;
334                } catch (NoSuchFieldException ignore) {
335                }
336                if (found != null) {
337                    split = Arrays.copyOfRange(split, 1, split.length);
338                    clazz = found;
339                    instance = clazz.getDeclaredConstructor().newInstance();
340                    continue;
341                }
342                return null;
343            }
344        } catch (Throwable e) {
345            e.printStackTrace();
346        }
347        return null;
348    }
349
350    /**
351     * Translate a node to a java field name.
352     *
353     * @param node
354     * @return
355     */
356    private static String toFieldName(String node) {
357        return node.toUpperCase().replaceAll("-", "_");
358    }
359
360    /**
361     * Translate a field to a config node.
362     *
363     * @param field
364     * @return
365     */
366    private static String toNodeName(String field) {
367        return field.toLowerCase().replace("_", "-");
368    }
369
370    /**
371     * Set some field to be accessible.
372     *
373     * @param field
374     */
375    private static void setAccessible(Field field) {
376        field.setAccessible(true);
377    }
378
379    /**
380     * Indicates that a field should be instantiated / created.
381     */
382    @Retention(RetentionPolicy.RUNTIME)
383    @Target({ElementType.FIELD})
384    public @interface Create {
385
386    }
387
388
389    /**
390     * Indicates that a field cannot be modified.
391     */
392    @Retention(RetentionPolicy.RUNTIME)
393    @Target({ElementType.FIELD})
394    public @interface Final {
395
396    }
397
398
399    /**
400     * Creates a comment.
401     */
402    @Retention(RetentionPolicy.RUNTIME)
403    @Target({ElementType.FIELD, ElementType.TYPE})
404    public @interface Comment {
405
406        String[] value();
407
408    }
409
410
411    /**
412     * The names of any default blocks.
413     */
414    @Retention(RetentionPolicy.RUNTIME)
415    @Target({ElementType.FIELD, ElementType.TYPE})
416    public @interface BlockName {
417
418        String[] value();
419
420    }
421
422
423    /**
424     * Any field or class with is not part of the config.
425     */
426    @Retention(RetentionPolicy.RUNTIME)
427    @Target({ElementType.FIELD, ElementType.TYPE})
428    public @interface Ignore {
429
430    }
431
432
433    @Ignore // This is not part of the config
434    public static class ConfigBlock<T> {
435
436        private final HashMap<String, T> INSTANCES = new HashMap<>();
437
438        public T get(String key) {
439            return INSTANCES.get(key);
440        }
441
442        public void put(String key, T value) {
443            INSTANCES.put(key, value);
444        }
445
446        public Collection<T> getInstances() {
447            return INSTANCES.values();
448        }
449
450        /**
451         * @deprecated Unused internally. Scheduled for removal in next major release.
452         */
453        @Deprecated(forRemoval = true, since = "6.11.1")
454        public Collection<String> getSections() {
455            return INSTANCES.keySet();
456        }
457
458        private Map<String, T> getRaw() {
459            return INSTANCES;
460        }
461
462    }
463
464}