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}