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}