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