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}