Skip to content

Commit 153f64a

Browse files
christiangoerdesrraystpredic8
authored
Add Yaml bean support (#2436)
* "Add support for excluding elements from JSON schema generation" * Extend Bean class with attributes and properties, remove unnecessary ID exclusion in JSON schema generation * Validate setId methods and add missing top-level id property When generating JSON schema for top-level @mcelement classes, enforce that any setId method is exactly setId(String), that at most one exists, and that it is annotated with @MCAttribute using the name "id" (or default). Throw ProcessingException on violations. If no @MCAttribute("id") is present, add an implicit optional "id" string property to the top-level schema. * Enforce void return type for setId(String) methods and improve error messages * Include child elements excludedFromJsonSchema when generating JSON Schema; extract bean id from YAML and fix bean index increment - Comment out early return for cei.excludedFromJsonSchema() so child elements are still added to schema (avoids validation failures) - Add extractIdOrDefault to GenericYamlParser to use an explicit "id" from the bean node when present - Use kind variable and correct idx++ placement when creating BeanDefinition entries * Introduce rootDef to MCElement and add root components support Add a boolean rootDef() to MCElement (with javadoc) to mark elements allowed at the config root. Mark APIProxy as a root-def element. Add new com.predic8.membrane.core.kubernetes.Components class annotated as a root-def element to represent top-level components with a child elements list and getter/setter. * Support flow parser types and root defs in JSON schema generation; remove legacy bean; mark Components noEnvelope; relax noEnvelope/topLevel check - Rename shouldGenerateParserType to shouldGenerateFlowParserType and update all call sites in JsonSchemaGenerator - Filter root definitions using rootDef() when generating JSON schema top-level defs - Adjust parser creation logic for flow parser refs - Remove legacy kubernetes bean field and its getter/setter from Bean.java - Mark Components as noEnvelope = true on MCElement - Comment out strict check in SpringConfigurationXSDGeneratingAnnotationProcessor for noEnvelope/topLevel and add TODO notes * Rename addTopLevelProperties to addRootLevelProperties, update method functionality and comments for clarity. * Add support for `allOf` in SchemaObject and adjust MCElement annotations in Components - Introduce `allOf` field and related functionality in SchemaObject for JSON schema generation. - Update Components class to adjust MCElement annotations by commenting out `noEnvelope` attribute. * Mark Components as noEnvelope and temporarily disable envelope handling for list items in K8sJsonSchemaGenerator * rename topLevel to component * rename topLevel to component * rename topLevel to component * rename rootDef to topLevel * rename rootDef to topLevel * Generate component-only JSON Schema defs and reference them in components lists Add createComponentParser and call ensureValidIdSetter for component-only elements. Make components without a real id gain an optional "id" property; alias schemas for no-envelope or elements with a real id. Update addChildsAsProperties to use component-specific $defs names when in a components list context. Add helpers: isComponentsList, componentDefName, hasRealIdAttribute. * revert GenericYamlParser changes * Add first version of using refs to reference component: current use: components: - basicAuthentication: id: asd --- api: port: 2000 flow: - basicAuthentication: $ref: asd ------------------------------------------------------ Generate component-or-ref schema definitions and use them for components - Add createComponentOrRefParser(Model, ElementInfo) to produce a oneOf schema that accepts either the inline component object or a {$ref: string}; handle noEnvelope case by returning a simple ref for now. - Add componentOrRefDefName helper to name "OrRef" defs. - Register component-or-ref parser for each element definition. - Update component name resolution: when an element is a component, use componentDefName in components context, otherwise use componentOrRefDefName so references point to the new oneOf definition. - Adjust list child processing to reference the OrRef definition for component children. * Allow list items to be direct $ref component references Add support for referencing a component instance directly at the list-item level (allow "- $ref: ...") when the list can contain component types. Make component schemas simpler: only the components list gets the id-augmented schema name, remove the separate createComponentOrRefParser and related helpers, and simplify createComponentParser behavior. Update addChildsAsProperties signature and adjust schema property requirements accordingly. * Generate patternProperties for components map and handle otherAttributes value types - Change Components to store components as Map<String,Object> and use @MCOtherAttributes setter. - Extend OtherAttributesInfo to detect Map value type (String or Object) and expose ValueType enum. - In JsonSchemaGenerator, treat @MCOtherAttributes with non-string values as an object map and create a special components parser that uses patternProperties with anyOf variants referencing $defs for component types. Only allow additionalProperties when otherAttributes map values are String. - Add SchemaObject.patternProperties support and include it in generated JSON output. * Allow object-level $ref for components in JSON schema When a type has a child element whose setter expects a component, add an optional "$ref" string property to the generated object schema so the object can reference a component via JSON Pointer. Avoid adding the property if a "$ref" already exists on the parser. Add hasComponentChild(ElementInfo, MainInfo) to detect component children and SchemaObject.hasProperty(String) helper to check existing properties. * Remove unnecessary TODO comments and restore validation logic for @mcelement annotations - Eliminated outdated and unused TODO comments. - Reintroduced validation for `noEnvelope=true` with conflicting attributes (e.g., `component=true` or `mixed=true`). * fixed YAMLParsingTests * generate the Components class automatically * Refactor `parseXML` and `parseYAML` logic to share context class loader utility - Introduced `withContextClassLoader` utility to streamline setting and restoring class loaders. - Replaced duplicated XML parsing logic with `parseXML` method utilizing `withContextClassLoader`. - Simplified class loader creation with `xmlClassLoader` for XML parsing. - Updated tests to use `parseXML` method for Spring XML configurations. * Enable `errorInListItemUniqueness` test and fix assertion message for schema violations * Add YAMLComponentsParsingTest, improve test utilities, and fix components setter NPE - Add YAMLComponentsParsingTest covering component refs and parsing scenarios. - Add assertStructure(List, Asserter...) helper to StructureAssertionUtil. - Refactor imports and static imports in CompilerHelper for clarity. - Initialize this.components and guard null input in generated ComponentClassGenerator#setComponents to avoid NPE when merging maps. * Support YAML "components" and allow unordered bean assertions - Parse top-level "components" mapping into BeanDefinition entries (name '#/components/<id>') - Exclude component beans from activation and from getBeans() listing - Add GenericYamlParser.extractComponentBeanDefinitions and wrap component nodes - Make StructureAssertionUtil.assertStructure match expected asserters in any order (backtracking matcher) and improve error reporting - Remove test that assumed a specific components/api ordering - Add small BeanDefinition javadoc for constructed bean field * Validate collection element types and strengthen $ref handling with clearer errors - Validate element types for Collection setters and throw ParsingException on mismatch. - Add getCollectionElementType helper and McYamlIntrospector.getElementName(). - Apply object-level $ref earlier; forbid mixing $ref with inline child and enforce allowed component types via grammar checks. - Improve error messages for component reference and inline+$ref conflicts and update tests accordingly. * Refactor MethodSetter to improve collection handling and remove unused $ref logic - Simplify collection parsing by cleaning imports and updating parseListIncludingStartEvent usage. - Extend getCollectionElementType to handle WildcardType upper bounds. - Remove obsolete $ref handler in GenericYamlParser. * Remove unused `lazyInit` property and related methods in `Bean` class - Clean up code by eliminating unused property `lazyInit` and associated getter, setter, and comment. * Add test for component referencing another component in YAML parsing - Introduce `componentRefersToAnotherComponent` test in `YAMLComponentsParsingTest`. - Verify nested component references and parsing correctness with assertions for structure. * Prevent top-level elements from being used as nested children in JSON schema - Update `JsonSchemaGenerator` to filter out top-level elements when adding nested properties. - Add tests in `YAMLComponentsParsingTest` to verify restrictions and allow correct placement of top-level elements. * Remove component-only JSON Schema parsers and id setter validation, tidy imports, and use method reference in McYamlIntrospector * Refactor `JsonSchemaGenerator` to use constants for "components" and ID pattern, remove unused `excludedFromJsonSchema` method - Replaced repeated string literals with `COMPONENTS` and `COMPONENT_ID_PATTERN` constants. - Cleaned up related `isComponentsMap` and `isComponentsList` logic to use constants. - Removed unused `excludedFromJsonSchema` method from `ChildElementInfo` to simplify codebase. * Add excludeFromFlow to @mcelement, mark interceptors excluded from flow, and simplify JsonSchemaGenerator * Refactor `$ref` handling in `GenericYamlParser` for improved error clarity and inline conflict prevention * Remove unused `excludeFromFlow` set and `componentDefName` method in `JsonSchemaGenerator` - Simplify `JsonSchemaGenerator` by cleaning up redundant code. - Remove unused `excludeFromFlow` set and `componentDefName` helper function for better maintainability. * Add documentation and implement missing attributes in `Bean`, `ConstructorArg`, and `Property` classes - Provide YAML configuration example for `Bean`. - Remove unused `id` property from `Bean`. - Add getters and setters for `value` and `ref` in `ConstructorArg` and `Property`. * Add BeanClassGenerator to produce Bean at compile time, invoke it from the annotation processor, and remove the old core Bean implementation * fix BeanClassGenerator * Add `BeanFactory` for dynamic bean instantiation and integrate prototype handling in `BeanRegistryImplementation` - Introduced `BeanFactory` to create Java objects from YAML "bean" nodes with support for constructor arguments and properties. - Enhanced `BeanRegistryImplementation` to distinguish and handle singleton vs. prototype beans. - Added `YAMLBeanParsingTest` for comprehensive validation of bean instantiation and lifecycle management. * Refactor `BeanRegistryImplementation` to improve error handling and simplify prototype scope checks - Replaced `IOException` with `RuntimeException` in `define` for better error propagation. - Simplified prototype scope check using default value in `JsonNode`. - Removed unused bean scopes in `BeanClassGenerator`. * Add example for custom interceptor using YAML configuration - Created `apis.yaml` to demonstrate defining and using a custom interceptor in the flow. - Updated `README.md` to reflect the new YAML-based example, replacing the previous XML format. * Refactor `BeanFactory` to improve class loading and enhance error assertions in `YAMLBeanParsingTest` - Added `loadBeanClass` method in `BeanFactory` to streamline and prioritize class loading from multiple class loaders. - Updated error assertions in `YAMLBeanParsingTest` for better coverage and clearer schema error validation. * Update license header in `YAMLBeanParsingTest` for compliance with Apache License 2.0. * coderabbit fixes * Refactor `ComponentClassGenerator` and `BeanClassGenerator` to extend abstract `ClassGenerator` - Introduced `ClassGenerator` as an abstract base class to encapsulate shared functionality. - Simplified `ComponentClassGenerator` and `BeanClassGenerator` by moving common logic to `ClassGenerator`. * minor * Update license headers for compliance and enhance bean reference support in YAML parsing - Added Apache License 2.0 headers to multiple files for compliance. - Improved YAML parsing in `MethodSetter` to support bean reference resolution with type checks. * fix Parsing * refactor: simplify `BeanFactory` code and streamline property parsing - Renamed `createFromNode()` to `create()` for clarity. - Introduced `Property` class for improved property handling. - Refactored redundant code for better readability and maintained behavior consistency. - Enhanced formatting of exception messages for uniformity. * Refactor `BeanFactory` to simplify constructor argument parsing with streamlined logic and improved readability. * coderabbit suggestions * refactor: improve code readability and modularity across `annot` package - Simplified key matching logic in `McYamlIntrospector` and introduced helper methods. - Extracted repetitive logic into reusable methods, e.g., `getReferenced` and `guardHasMCAttributeSetters`. - Introduced additional validation and utility methods in `BeanDefinition` and `BeanRegistryImplementation`. - Streamlined schema generation logic in `JsonSchemaGenerator`, reducing redundancy in object creation and property handling. - Enhanced `Grammar`'s source file creation and assembly logic for clearer responsibility separation. * refactor: remove unused methods and make lists immutable in `annot` package - Removed redundant getters and setters in `Grammar` and `ElementInfo`. - Marked `ais` and `ceis` lists in `ElementInfo` as `final` to ensure immutability. - Cleaned up unused return documentation in `McYamlIntrospector`. * refactor: streamline code and improve readability in `annot` package - Simplified property-checking logic using streams in `SchemaObject`. - Corrected method name typo from `stripFistLine` to `stripFirstLine` in `CompilerHelper`. - Replaced inner `Pair` class with `record` for better conciseness. - Improved formatting and consistency in generated content for `Grammar`. * feat: add ReflectionUtil with type conversion and primitive-wrapper utility methods - Introduced `ReflectionUtil` for converting string values to specified Java types. - Added `isWrapperOfPrimitive` method for validating primitive-wrapper type relationships. - Included comprehensive JUnit tests for all utility methods. * refactor: streamline `CompilerHelper` code and enhance modularity - Consolidated repetitive class loader overlay logic into a reusable `overlayClassLoader` method. - Simplified `withContextClassLoader` error handling for better clarity. - Resolved minor issues in object creation with `APPLICATION_CONTEXT_CLASSNAME` instance lifecycle management. - Improved imports organization and reduced redundancy in both structure and functionality. * Refactor `YAMLBeanParsingTest` to split tests by scope, update scope annotation in `BeanDefinition`, and clarify `BeanFactory` class loading logic * Add `Scope` enum, update exception type in `OtherAttributesInfo`, and import `Scope` in `BeanClassGenerator`. - Introduced `Scope` enum with `SINGLETON` and `PROTOTYPE` values. - Replaced `IllegalArgumentException` with `ProcessingException` for better exception handling in `OtherAttributesInfo`. - Added `Scope` import to `BeanClassGenerator`. * refactor: avoid NPE --------- Co-authored-by: Tobias Polley <mail@tobias-polley.de> Co-authored-by: Thomas Bayer <bayer@predic8.de>
1 parent 414b1ca commit 153f64a

121 files changed

Lines changed: 2624 additions & 681 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

annot/src/main/java/com/predic8/membrane/annot/MCChildElement.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@
3131
* Allows the child to come from a schema other than Membrane core. Used for spring beans, e.g. ref to ssl bean
3232
*/
3333
boolean allowForeign() default false;
34+
35+
boolean excludeFromJson() default false; // excludes from JSON Schema (YAML)
3436
}

annot/src/main/java/com/predic8/membrane/annot/MCElement.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@
2727

2828
boolean mixed() default false;
2929

30+
/**
31+
* Whether the element can be defined at the top-level of the config.
32+
*/
33+
boolean topLevel() default false;
34+
3035
/**
3136
* Whether the element can be a separate bean in the XML schema, or a separate document in YAML/JSON.
3237
*/
33-
boolean topLevel() default true;
38+
boolean component() default true;
3439

3540
String configPackage() default "";
3641

@@ -54,4 +59,9 @@
5459
* This does not have any effect on the XML grammar.
5560
*/
5661
boolean noEnvelope() default false;
62+
63+
/**
64+
* Whether the element should be configurable as part of the interceptor flow
65+
*/
66+
boolean excludeFromFlow() default false;
5767
}

annot/src/main/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessor.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,10 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
215215

216216
scan(main, ii);
217217

218-
if (ii.getAnnotation().topLevel())
219-
main.getTopLevels().put(ii.getAnnotation().name(), ii);
218+
if (ii.getAnnotation().component())
219+
main.getComponents().put(ii.getAnnotation().name(), ii);
220220

221221
if (ii.getAnnotation().noEnvelope()) {
222-
if (ii.getAnnotation().topLevel())
223-
throw new ProcessingException("@MCElement(..., noEnvelope=true, topLevel=true) is invalid.", ii.getElement());
224222
if (ii.getAnnotation().mixed())
225223
throw new ProcessingException("@MCElement(..., noEnvelope=true, mixed=true) is invalid.", ii.getElement());
226224
if (ii.getChildElementSpecs().size() != 1)
@@ -254,8 +252,8 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
254252
for (Entry<TypeElement, ElementInfo> e : main.getElements().entrySet()) {
255253
if (!processingEnv.getTypeUtils().isAssignable(e.getKey().asType(), f.getKey().asType()))
256254
continue;
257-
if (targetIsObject && !isTopLevelMCElement(e.getKey()))
258-
continue; // only allow topLevel MCElements for Object
255+
if (targetIsObject && !isComponent(e.getKey()))
256+
continue; // only allow component MCElements for Object
259257
cedi.getElementInfo().add(e.getValue());
260258
}
261259
}
@@ -272,9 +270,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
272270

273271
for (MainInfo main : m.getMains()) {
274272
for (Entry<TypeElement, ElementInfo> f : main.getElements().entrySet()) {
275-
ElementInfo ei2 = main.getTopLevels().get(f.getKey().getAnnotation(MCElement.class).name());
276-
if (ei2 != null && f.getValue() != ei2 && f.getValue().getAnnotation().topLevel())
277-
throw new ProcessingException("Duplicate top-level @MCElement name. Make at least one @MCElement(topLevel=false,...) .", f.getKey(), ei2.getElement());
273+
ElementInfo ei2 = main.getComponents().get(f.getKey().getAnnotation(MCElement.class).name());
274+
if (ei2 != null && f.getValue() != ei2 && f.getValue().getAnnotation().component())
275+
throw new ProcessingException("Duplicate component @MCElement name. Make at least one @MCElement(component=false,...) .", f.getKey(), ei2.getElement());
278276

279277
List<String> uniquenessErrors = getUniquenessError(f.getValue(), main);
280278
if (!uniquenessErrors.isEmpty())
@@ -299,9 +297,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
299297
}
300298
}
301299

302-
private boolean isTopLevelMCElement(TypeElement type) {
300+
private boolean isComponent(TypeElement type) {
303301
MCElement mcElement = type.getAnnotation(MCElement.class);
304-
return (mcElement != null) && mcElement.topLevel();
302+
return (mcElement != null) && mcElement.component();
305303
}
306304

307305
private List<String> getUniquenessError(ElementInfo ii, MainInfo main) {
@@ -512,6 +510,10 @@ private boolean isRequired(Element e2) {
512510
}
513511

514512
public void process(Model m) throws IOException {
513+
if (new ComponentClassGenerator(processingEnv).writeJava(m))
514+
return; // we will be called again to handle the newly generated class.
515+
if (new BeanClassGenerator(processingEnv).writeJava(m))
516+
return; // we will be called again to handle the newly generated class.
515517
new Schemas(processingEnv).writeXSD(m);
516518
new KubernetesBootstrapper(processingEnv).boot(m);
517519
new JsonSchemaGenerator(processingEnv).write(m);
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
15+
package com.predic8.membrane.annot.bean;
16+
17+
import com.fasterxml.jackson.databind.*;
18+
import com.predic8.membrane.annot.util.*;
19+
import com.predic8.membrane.annot.yaml.*;
20+
import org.jetbrains.annotations.*;
21+
22+
import java.lang.reflect.*;
23+
import java.util.*;
24+
import java.util.stream.*;
25+
26+
import static com.predic8.membrane.annot.util.ReflectionUtil.isWrapperOfPrimitive;
27+
28+
/**
29+
* Builds Java objects from a "bean" JSON node (YAML).
30+
*/
31+
public final class BeanFactory {
32+
33+
private final BeanRegistry registry;
34+
35+
public BeanFactory(BeanRegistry registry) {
36+
this.registry = registry;
37+
}
38+
39+
/**
40+
* Creates an instance described by the given bean node.
41+
*/
42+
public Object create(JsonNode beanBody) {
43+
String className = getTextContent(beanBody, "class");
44+
45+
try {
46+
Object instance = instantiate(
47+
loadBeanClass(className),
48+
parseConstructorArgList(beanBody.path("constructorArgs"))
49+
);
50+
applyProperties(instance, parsePropertyList(beanBody.path("properties")));
51+
return instance;
52+
} catch (Exception e) {
53+
throw new RuntimeException("Could not create bean for class: " + className, e);
54+
}
55+
}
56+
57+
// TODO simplify this. 'normal' code should not be required to use classloader magic
58+
private Class<?> loadBeanClass(String className) throws ClassNotFoundException {
59+
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
60+
if (classLoader != null) {
61+
try {
62+
return Class.forName(className, true, classLoader);
63+
} catch (ClassNotFoundException ignored) {
64+
}
65+
}
66+
67+
classLoader = registry.getGrammar().getClass().getClassLoader();
68+
if (classLoader != null) {
69+
try {
70+
return Class.forName(className, true, classLoader);
71+
} catch (ClassNotFoundException ignored) {
72+
}
73+
}
74+
75+
classLoader = BeanFactory.class.getClassLoader();
76+
if (classLoader != null) {
77+
try {
78+
return Class.forName(className, true, classLoader);
79+
} catch (ClassNotFoundException ignored) {
80+
}
81+
}
82+
83+
return Class.forName(className);
84+
}
85+
86+
private class ConstructorArg {
87+
String value, ref;
88+
89+
public ConstructorArg(JsonNode node) {
90+
var item = node.isObject() && node.has("constructorArg") ? node.get("constructorArg") : node;
91+
92+
value = getTextOrNull(item, "value");
93+
ref = getTextOrNull(item, "ref");
94+
}
95+
}
96+
97+
private class Property {
98+
String name, value, ref;
99+
100+
public Property(JsonNode node) {
101+
var item = node.isObject() && node.has("property") ? node.get("property") : node;
102+
103+
name = getTextContent(item, "name");
104+
value = getTextOrNull(item, "value");
105+
ref = getTextOrNull(item, "ref");
106+
}
107+
108+
public boolean isBlank() {
109+
return name == null || name.isBlank();
110+
}
111+
}
112+
113+
private List<ConstructorArg> parseConstructorArgList(JsonNode arr) {
114+
if (arr == null || !arr.isArray()) return List.of();
115+
116+
return StreamSupport.stream(arr.spliterator(), false)
117+
.map(ConstructorArg::new)
118+
.toList();
119+
}
120+
121+
private List<Property> parsePropertyList(JsonNode arr) {
122+
if (arr == null || !arr.isArray()) return List.of();
123+
124+
return StreamSupport.stream(arr.spliterator(), false)
125+
.map(Property::new)
126+
.toList();
127+
}
128+
129+
private String getTextContent(JsonNode n, String key) {
130+
JsonNode v = n.get(key);
131+
if (v == null || !v.isTextual() || v.asText().isBlank())
132+
throw new IllegalArgumentException("Missing/blank '" + key + "' in bean spec.");
133+
return v.asText();
134+
}
135+
136+
private String getTextOrNull(JsonNode n, String key) {
137+
JsonNode v = n.get(key);
138+
return v != null && v.isTextual() ? v.asText() : null;
139+
}
140+
141+
private Object instantiate(Class<?> type, List<ConstructorArg> args) throws Exception {
142+
int n = args == null ? 0 : args.size();
143+
144+
Set<Constructor<?>> constructors = new LinkedHashSet<>();
145+
constructors.addAll(Arrays.asList(type.getConstructors()));
146+
constructors.addAll(Arrays.asList(type.getDeclaredConstructors()));
147+
148+
Constructor<?> best = null;
149+
Object[] bestArgs = null;
150+
151+
for (Constructor<?> c : constructors) {
152+
if (c.getParameterCount() != n) continue;
153+
Object[] resolved = tryResolveCtorArgs(c.getParameterTypes(), args);
154+
if (resolved != null) {
155+
best = c;
156+
bestArgs = resolved;
157+
break;
158+
}
159+
}
160+
161+
if (best == null) {
162+
throw new IllegalArgumentException("No matching constructor found for %s with %d argument(s).".formatted(type.getName(), n));
163+
}
164+
165+
best.setAccessible(true);
166+
return best.newInstance(bestArgs);
167+
}
168+
169+
private Object[] tryResolveCtorArgs(Class<?>[] paramTypes, List<ConstructorArg> args) {
170+
try {
171+
Object[] resolved = new Object[paramTypes.length];
172+
for (int i = 0; i < paramTypes.length; i++) {
173+
resolved[i] = resolveValueOrRef(paramTypes[i], args.get(i).value, args.get(i).ref);
174+
}
175+
return resolved;
176+
} catch (Exception e) {
177+
return null; // not compatible
178+
}
179+
}
180+
181+
private void applyProperties(Object target, List<Property> props) throws Exception {
182+
for (Property p : props) {
183+
if (p.isBlank())
184+
throw new IllegalArgumentException("Property name must not be blank.");
185+
186+
Method setter = findSetter(target.getClass(), p.name);
187+
if (setter != null) {
188+
Class<?> pt = setter.getParameterTypes()[0];
189+
setter.setAccessible(true);
190+
setter.invoke(target, resolveValueOrRef(pt, p.value, p.ref));
191+
continue;
192+
}
193+
194+
Field f = findField(target.getClass(), p.name);
195+
if (f != null) {
196+
f.setAccessible(true);
197+
f.set(target, resolveValueOrRef(f.getType(), p.value, p.ref));
198+
continue;
199+
}
200+
201+
throw new IllegalArgumentException("No setter/field found for property '%s' on %s".formatted(p.name, target.getClass().getName()));
202+
}
203+
}
204+
205+
private Method findSetter(Class<?> clazz, String prop) {
206+
String setterName = getSetterName(prop);
207+
for (Method method : clazz.getMethods()) {
208+
if (matchesSetter(method, setterName)) return method;
209+
}
210+
for (Method method : clazz.getDeclaredMethods()) {
211+
if (matchesSetter(method, setterName)) return method;
212+
}
213+
return null;
214+
}
215+
216+
private static boolean matchesSetter(Method method, String setterName) {
217+
return method.getName().equals(setterName) && method.getParameterCount() == 1;
218+
}
219+
220+
// e.g. bar -> setBar
221+
private static @NotNull String getSetterName(String prop) {
222+
if (prop == null || prop.isEmpty()) {
223+
throw new IllegalArgumentException("Property name cannot be null or empty");
224+
}
225+
return "set" + Character.toUpperCase(prop.charAt(0)) + prop.substring(1);
226+
}
227+
228+
private Field findField(Class<?> clazz, String name) {
229+
Class<?> c = clazz;
230+
while (c != null && c != Object.class) {
231+
try {
232+
return c.getDeclaredField(name);
233+
} catch (NoSuchFieldException ignored) {
234+
c = c.getSuperclass();
235+
}
236+
}
237+
return null;
238+
}
239+
240+
private Object resolveValueOrRef(Class<?> targetType, String value, String ref) {
241+
if (ref != null && !ref.isBlank()) {
242+
Object o = registry.resolveReference(ref);
243+
if (o != null && !targetType.isInstance(o)) {
244+
if (!(targetType.isPrimitive() && isWrapperOfPrimitive(targetType, o.getClass()))) {
245+
throw new IllegalArgumentException("Ref '%s' is not assignable to %s".formatted(ref, targetType.getName()));
246+
}
247+
}
248+
return o;
249+
}
250+
return ReflectionUtil.convert(value, targetType);
251+
}
252+
}

0 commit comments

Comments
 (0)