Skip to content

Commit b23c0d3

Browse files
Make determining the backing field of a getter more stable
1 parent 0c8d169 commit b23c0d3

3 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/main/java/dev/chojo/ocular/override/OverrideApplier.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,80 @@ public static <V> void applyOverrides(V object, ValueSupplier supplier) {
102102
}
103103
}
104104
}
105+
106+
// Handle @Overwrite annotations on zero-parameter methods (getters).
107+
// When a getter like "greetingValue()" is annotated, the override is keyed by the method
108+
// name. Since we can't "set" a value through a getter, we need to find the backing field.
109+
// Strategy: look for a field whose name the method name starts with (e.g. "greetingValue"
110+
// starts with "greeting"), or try stripping common getter prefixes like "get"/"is".
111+
for (Method method : clazz.getDeclaredMethods()) {
112+
if (method.getParameterCount() != 0) continue;
113+
Optional<Object> override = supplier.getValue(method.getName());
114+
if (override.isPresent()) {
115+
Field targetField = findBackingField(clazz, method.getName(), method.getReturnType());
116+
if (targetField != null) {
117+
try {
118+
targetField.setAccessible(true);
119+
Object value = convertValue(targetField.getType(), override.get());
120+
if (value != null) {
121+
targetField.set(object, value);
122+
}
123+
} catch (IllegalAccessException e) {
124+
log.warn("Could not set override for getter {}: {}", method.getName(), e.getMessage());
125+
} catch (NumberFormatException e) {
126+
log.warn("Could not convert override value for getter {}: {}", method.getName(), e.getMessage());
127+
}
128+
} else {
129+
log.warn("Could not find backing field for getter method {}", method.getName());
130+
}
131+
}
132+
}
133+
}
134+
135+
/**
136+
* Finds the backing field for a getter method by trying several strategies:
137+
* <ol>
138+
* <li>Exact name match (method name equals field name)</li>
139+
* <li>JavaBean getter convention: strip "get"/"is" prefix and lowercase first char</li>
140+
* <li>Prefix match: find a field whose name the method name starts with,
141+
* preferring the longest matching field name (e.g. "greetingValue" matches "greeting")</li>
142+
* </ol>
143+
* Only fields whose type is compatible with the method's return type are considered.
144+
*/
145+
private static Field findBackingField(Class<?> clazz, String methodName, Class<?> returnType) {
146+
// Strategy 1: exact name match
147+
for (Field field : clazz.getDeclaredFields()) {
148+
if (field.getName().equals(methodName) && field.getType().equals(returnType)) {
149+
return field;
150+
}
151+
}
152+
153+
// Strategy 2: JavaBean getter convention (getHost -> host, isDebug -> debug)
154+
String beanFieldName = null;
155+
if (methodName.startsWith("get") && methodName.length() > 3) {
156+
beanFieldName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
157+
} else if (methodName.startsWith("is") && methodName.length() > 2) {
158+
beanFieldName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
159+
}
160+
if (beanFieldName != null) {
161+
for (Field field : clazz.getDeclaredFields()) {
162+
if (field.getName().equals(beanFieldName) && field.getType().equals(returnType)) {
163+
return field;
164+
}
165+
}
166+
}
167+
168+
// Strategy 3: find the field whose name is the longest prefix of the method name
169+
Field bestMatch = null;
170+
int bestLength = 0;
171+
for (Field field : clazz.getDeclaredFields()) {
172+
String fname = field.getName();
173+
if (methodName.startsWith(fname) && fname.length() > bestLength && field.getType().equals(returnType)) {
174+
bestMatch = field;
175+
bestLength = fname.length();
176+
}
177+
}
178+
return bestMatch;
105179
}
106180

107181
/**

src/test/java/dev/chojo/classes/JacksonOverrideConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public JacksonOverrideConfig(@JsonProperty("host") String host,
3838
this.greeting = greeting;
3939
}
4040

41-
@Overwrite(sys = @SysProp("config.greeting"), env = @EnvVar("CONFIG_GREETING"))
4241
public void greeting(String greeting) {
4342
this.greeting = greeting;
4443
}
@@ -55,6 +54,7 @@ public boolean debug() {
5554
return debug;
5655
}
5756

57+
@Overwrite(sys = @SysProp("config.greeting"), env = @EnvVar("CONFIG_GREETING"))
5858
public String greeting() {
5959
return greeting;
6060
}

src/test/java/dev/chojo/ocular/JacksonOverrideTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ void methodOverrideViaSysProp() {
8989
assertEquals("localhost", config.host());
9090
}
9191

92+
@Test
93+
void greetingOverwrittenViaSysProp() {
94+
System.setProperty("config.greeting", "overridden-greeting");
95+
96+
JacksonOverrideConfig config = loadViaConfigurations();
97+
98+
// The greeting field is private and only accessible via the greeting() method,
99+
// which is annotated with @Overwrite. Verify the override is applied.
100+
assertEquals("overridden-greeting", config.greeting());
101+
// Original values for other fields remain unchanged
102+
assertEquals("localhost", config.host());
103+
assertEquals(8080, config.port());
104+
assertFalse(config.debug());
105+
}
106+
92107
@Test
93108
void fieldAndMethodOverridesCombined() {
94109
System.setProperty("config.host", "override-host");

0 commit comments

Comments
 (0)