|
| 1 | +/* |
| 2 | + * SPDX-License-Identifier: LGPL-3.0-or-later |
| 3 | + * |
| 4 | + * Copyright (C) RainbowDashLabs and Contributor |
| 5 | + */ |
| 6 | +package dev.chojo.ocular.override; |
| 7 | + |
| 8 | +import org.slf4j.Logger; |
| 9 | + |
| 10 | +import java.lang.reflect.Field; |
| 11 | +import java.lang.reflect.Method; |
| 12 | +import java.util.Optional; |
| 13 | + |
| 14 | +import static org.slf4j.LoggerFactory.getLogger; |
| 15 | + |
| 16 | +/** |
| 17 | + * Applies override values from a {@link ValueSupplier} to a configuration object at runtime. |
| 18 | + * <p> |
| 19 | + * This is the runtime counterpart to the compile-time code generation done by |
| 20 | + * {@link dev.chojo.ocular.processor.OcularProcessor}. The overall flow is: |
| 21 | + * <ol> |
| 22 | + * <li>At compile time, the annotation processor scans {@link Overwrite @Overwrite} annotations |
| 23 | + * and generates a {@link ValueSupplier} class that reads env vars / system properties.</li> |
| 24 | + * <li>At runtime, after a config file is deserialized, {@link dev.chojo.ocular.Configurations} |
| 25 | + * loads the generated {@link ValueSupplier} via reflection.</li> |
| 26 | + * <li>This class then iterates over every field and single-parameter method of the config object, |
| 27 | + * asks the supplier if an override exists for that name, converts the string value to the |
| 28 | + * correct type, and sets it on the object — effectively replacing the file-based value.</li> |
| 29 | + * </ol> |
| 30 | + */ |
| 31 | +public final class OverrideApplier { |
| 32 | + private static final Logger log = getLogger(OverrideApplier.class); |
| 33 | + |
| 34 | + private OverrideApplier() {} |
| 35 | + |
| 36 | + /** |
| 37 | + * Applies all available overrides from the given supplier to the configuration object. |
| 38 | + * <p> |
| 39 | + * For each declared field, it checks if the supplier has an override value for that field name. |
| 40 | + * If so, the value is converted from a string to the field's type and written directly into the field. |
| 41 | + * The same is done for single-parameter methods (e.g. setters), where the override value is |
| 42 | + * converted to the method's parameter type and the method is invoked. |
| 43 | + * |
| 44 | + * @param object the configuration object whose fields/methods may be overridden |
| 45 | + * @param supplier the source of override values (typically a generated class) |
| 46 | + * @param <V> the configuration type |
| 47 | + */ |
| 48 | + public static <V> void applyOverrides(V object, ValueSupplier supplier) { |
| 49 | + if (object == null || supplier == null) return; |
| 50 | + |
| 51 | + // Get the class definition so we can inspect its fields and methods via reflection. |
| 52 | + // Reflection lets us read and write private fields at runtime, which is how we inject |
| 53 | + // override values without the config class needing any special code. |
| 54 | + Class<?> clazz = object.getClass(); |
| 55 | + |
| 56 | + // Walk through every field in the config class (e.g. "private String host") |
| 57 | + // and check if the generated supplier has an override value for that field name. |
| 58 | + for (Field field : clazz.getDeclaredFields()) { |
| 59 | + Optional<Object> override = supplier.getValue(field.getName()); |
| 60 | + if (override.isPresent()) { |
| 61 | + try { |
| 62 | + // By default, Java prevents access to private fields from outside the class. |
| 63 | + // setAccessible(true) bypasses that restriction so we can write to it. |
| 64 | + field.setAccessible(true); |
| 65 | + // The override value comes as a String (from env var / sys prop), but the field |
| 66 | + // might be an int, boolean, etc. convertValue handles that conversion. |
| 67 | + Object value = convertValue(field.getType(), override.get()); |
| 68 | + if (value != null) { |
| 69 | + // Actually write the override value into the field on the config object |
| 70 | + field.set(object, value); |
| 71 | + } |
| 72 | + } catch (IllegalAccessException e) { |
| 73 | + log.warn("Could not set override for field {}: {}", field.getName(), e.getMessage()); |
| 74 | + } catch (NumberFormatException e) { |
| 75 | + log.warn("Could not convert override value for field {}: {}", field.getName(), e.getMessage()); |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // Also check single-parameter methods (typically setters like "setHost(String host)"). |
| 81 | + // We only consider methods with exactly one parameter, since those are the ones that |
| 82 | + // make sense as "set this value" operations. |
| 83 | + for (Method method : clazz.getDeclaredMethods()) { |
| 84 | + if (method.getParameterCount() != 1) continue; |
| 85 | + Optional<Object> override = supplier.getValue(method.getName()); |
| 86 | + if (override.isPresent()) { |
| 87 | + try { |
| 88 | + method.setAccessible(true); |
| 89 | + // Convert the string value to match the method's parameter type |
| 90 | + Object value = convertValue(method.getParameterTypes()[0], override.get()); |
| 91 | + if (value != null) { |
| 92 | + // Call the method on the config object, passing the override value |
| 93 | + method.invoke(object, value); |
| 94 | + } |
| 95 | + } catch (IllegalAccessException e) { |
| 96 | + log.warn("Could not invoke override for method {}: {}", method.getName(), e.getMessage()); |
| 97 | + } catch (NumberFormatException e) { |
| 98 | + log.warn("Could not convert override value for method {}: {}", method.getName(), e.getMessage()); |
| 99 | + } catch (java.lang.reflect.InvocationTargetException e) { |
| 100 | + log.warn("Override method {} threw an exception: {}", method.getName(), e.getCause().getMessage()); |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Converts a string override value to the target field/parameter type. |
| 108 | + * Supports all Java primitive types and their boxed equivalents, plus String. |
| 109 | + * Returns null for unsupported types (with a warning logged). |
| 110 | + */ |
| 111 | + private static Object convertValue(Class<?> type, Object value) { |
| 112 | + if (value == null) return null; |
| 113 | + // Environment variables and system properties are always strings, so we need to parse |
| 114 | + // them into the correct Java type. For example, the string "8080" becomes the int 8080. |
| 115 | + String stringValue = value.toString(); |
| 116 | + |
| 117 | + if (type == String.class) return stringValue; |
| 118 | + if (type == int.class || type == Integer.class) return Integer.parseInt(stringValue); |
| 119 | + if (type == long.class || type == Long.class) return Long.parseLong(stringValue); |
| 120 | + if (type == boolean.class || type == Boolean.class) return Boolean.parseBoolean(stringValue); |
| 121 | + if (type == double.class || type == Double.class) return Double.parseDouble(stringValue); |
| 122 | + if (type == float.class || type == Float.class) return Float.parseFloat(stringValue); |
| 123 | + if (type == short.class || type == Short.class) return Short.parseShort(stringValue); |
| 124 | + if (type == byte.class || type == Byte.class) return Byte.parseByte(stringValue); |
| 125 | + |
| 126 | + log.warn("Unsupported override type: {}", type.getName()); |
| 127 | + return null; |
| 128 | + } |
| 129 | +} |
0 commit comments