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}