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