-
Notifications
You must be signed in to change notification settings - Fork 167
Add Yaml bean support #2436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add Yaml bean support #2436
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
aa5a565
"Add support for excluding elements from JSON schema generation"
christiangoerdes 5cdbd9f
Extend Bean class with attributes and properties, remove unnecessary …
christiangoerdes 2f9d626
Validate setId methods and add missing top-level id property
christiangoerdes 8972e3f
Enforce void return type for setId(String) methods and improve error …
christiangoerdes 6463a94
Include child elements excludedFromJsonSchema when generating JSON Sc…
christiangoerdes d97651d
Introduce rootDef to MCElement and add root components support
christiangoerdes 689d841
Support flow parser types and root defs in JSON schema generation; re…
christiangoerdes 1fb6edf
Rename addTopLevelProperties to addRootLevelProperties, update method…
christiangoerdes 9a52b1b
Add support for `allOf` in SchemaObject and adjust MCElement annotati…
christiangoerdes 8edbc07
Mark Components as noEnvelope and temporarily disable envelope handli…
christiangoerdes e578ab1
rename topLevel to component
christiangoerdes 8655dd5
rename topLevel to component
christiangoerdes c30fb10
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes 055ce00
rename topLevel to component
christiangoerdes fa783c3
Merge branch 'master' into yaml-bean-support
christiangoerdes c183a44
rename rootDef to topLevel
christiangoerdes a531e0f
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes ff1c870
rename rootDef to topLevel
christiangoerdes 256d97c
Generate component-only JSON Schema defs and reference them in compon…
christiangoerdes 6e86af4
revert GenericYamlParser changes
christiangoerdes 4dd0bd1
Add first version of using refs to reference component:
christiangoerdes 8b0e7bb
Allow list items to be direct $ref component references
christiangoerdes a265cff
Generate patternProperties for components map and handle otherAttribu…
christiangoerdes 7ac913f
Allow object-level $ref for components in JSON schema
christiangoerdes c7b2a5e
Remove unnecessary TODO comments and restore validation logic for @MC…
christiangoerdes a514d94
fixed YAMLParsingTests
rrayst e962dd0
generate the Components class automatically
rrayst 80d4101
Merge branch 'master' into yaml-bean-support
christiangoerdes fe2fe39
Refactor `parseXML` and `parseYAML` logic to share context class load…
christiangoerdes 6a45ebf
Enable `errorInListItemUniqueness` test and fix assertion message for…
christiangoerdes fbfdc4a
Add YAMLComponentsParsingTest, improve test utilities, and fix compon…
christiangoerdes 5b215e5
Support YAML "components" and allow unordered bean assertions
christiangoerdes b8960d4
Validate collection element types and strengthen $ref handling with c…
christiangoerdes 7e4c450
Refactor MethodSetter to improve collection handling and remove unuse…
christiangoerdes fded354
Remove unused `lazyInit` property and related methods in `Bean` class
christiangoerdes 0d2b22b
Add test for component referencing another component in YAML parsing
christiangoerdes fcc854f
Merge branch 'master' into yaml-bean-support
christiangoerdes c4588e9
Prevent top-level elements from being used as nested children in JSON…
christiangoerdes 75516f6
Remove component-only JSON Schema parsers and id setter validation, t…
christiangoerdes 2949e01
Merge branch 'master' into yaml-bean-support
christiangoerdes c52e2ff
Refactor `JsonSchemaGenerator` to use constants for "components" and …
christiangoerdes 5263072
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes 6556e33
Add excludeFromFlow to @MCElement, mark interceptors excluded from fl…
christiangoerdes b3b6bc7
Refactor `$ref` handling in `GenericYamlParser` for improved error cl…
christiangoerdes 8bdffd0
Remove unused `excludeFromFlow` set and `componentDefName` method in …
christiangoerdes 363c25b
Add documentation and implement missing attributes in `Bean`, `Constr…
christiangoerdes 674b4c7
Add BeanClassGenerator to produce Bean at compile time, invoke it fro…
christiangoerdes 6790b2b
fix BeanClassGenerator
christiangoerdes 0fa19ea
Add `BeanFactory` for dynamic bean instantiation and integrate protot…
christiangoerdes eb92ce4
Refactor `BeanRegistryImplementation` to improve error handling and s…
christiangoerdes 25083cb
Add example for custom interceptor using YAML configuration
christiangoerdes 84f8d1f
Refactor `BeanFactory` to improve class loading and enhance error ass…
christiangoerdes f97cc07
Update license header in `YAMLBeanParsingTest` for compliance with Ap…
christiangoerdes 63405f2
coderabbit fixes
christiangoerdes bb238ab
Merge branch 'master' into yaml-bean-support
christiangoerdes e097e7c
Refactor `ComponentClassGenerator` and `BeanClassGenerator` to extend…
christiangoerdes 8221115
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes 4e61efa
minor
christiangoerdes 2d6a1f9
Update license headers for compliance and enhance bean reference supp…
christiangoerdes 2d2459b
fix Parsing
christiangoerdes 4509a02
refactor: simplify `BeanFactory` code and streamline property parsing
predic8 31f83ee
Refactor `BeanFactory` to simplify constructor argument parsing with …
christiangoerdes 3b104ec
coderabbit suggestions
christiangoerdes c8867b3
refactor: improve code readability and modularity across `annot` package
predic8 11290b3
refactor: remove unused methods and make lists immutable in `annot` p…
predic8 2f19129
refactor: streamline code and improve readability in `annot` package
predic8 e24a672
feat: add ReflectionUtil with type conversion and primitive-wrapper u…
predic8 7edab1a
refactor: streamline `CompilerHelper` code and enhance modularity
predic8 f94438c
Refactor `YAMLBeanParsingTest` to split tests by scope, update scope …
christiangoerdes cf7eb0d
Add `Scope` enum, update exception type in `OtherAttributesInfo`, and…
christiangoerdes a31ddba
refactor: avoid NPE
rrayst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
annot/src/main/java/com/predic8/membrane/annot/bean/BeanFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| /* Copyright 2025 predic8 GmbH, www.predic8.com | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. */ | ||
|
|
||
| package com.predic8.membrane.annot.bean; | ||
|
|
||
| import com.fasterxml.jackson.databind.*; | ||
| import com.predic8.membrane.annot.util.*; | ||
| import com.predic8.membrane.annot.yaml.*; | ||
| import org.jetbrains.annotations.*; | ||
|
|
||
| import java.lang.reflect.*; | ||
| import java.util.*; | ||
| import java.util.stream.*; | ||
|
|
||
| import static com.predic8.membrane.annot.util.ReflectionUtil.isWrapperOfPrimitive; | ||
|
|
||
| /** | ||
| * Builds Java objects from a "bean" JSON node (YAML). | ||
| */ | ||
| public final class BeanFactory { | ||
|
|
||
| private final BeanRegistry registry; | ||
|
|
||
| public BeanFactory(BeanRegistry registry) { | ||
| this.registry = registry; | ||
| } | ||
|
|
||
| /** | ||
| * Creates an instance described by the given bean node. | ||
| */ | ||
| public Object create(JsonNode beanBody) { | ||
| String className = getTextContent(beanBody, "class"); | ||
|
|
||
| try { | ||
| Object instance = instantiate( | ||
| loadBeanClass(className), | ||
| parseConstructorArgList(beanBody.path("constructorArgs")) | ||
| ); | ||
| applyProperties(instance, parsePropertyList(beanBody.path("properties"))); | ||
| return instance; | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Could not create bean for class: " + className, e); | ||
| } | ||
| } | ||
|
|
||
| // TODO simplify this. 'normal' code should not be required to use classloader magic | ||
| private Class<?> loadBeanClass(String className) throws ClassNotFoundException { | ||
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); | ||
| if (classLoader != null) { | ||
| try { | ||
| return Class.forName(className, true, classLoader); | ||
| } catch (ClassNotFoundException ignored) { | ||
| } | ||
| } | ||
|
|
||
| classLoader = registry.getGrammar().getClass().getClassLoader(); | ||
| if (classLoader != null) { | ||
| try { | ||
| return Class.forName(className, true, classLoader); | ||
| } catch (ClassNotFoundException ignored) { | ||
| } | ||
| } | ||
|
|
||
| classLoader = BeanFactory.class.getClassLoader(); | ||
| if (classLoader != null) { | ||
| try { | ||
| return Class.forName(className, true, classLoader); | ||
| } catch (ClassNotFoundException ignored) { | ||
| } | ||
| } | ||
|
|
||
| return Class.forName(className); | ||
| } | ||
|
|
||
| private class ConstructorArg { | ||
| String value, ref; | ||
|
|
||
| public ConstructorArg(JsonNode node) { | ||
| var item = node.isObject() && node.has("constructorArg") ? node.get("constructorArg") : node; | ||
|
|
||
| value = getTextOrNull(item, "value"); | ||
| ref = getTextOrNull(item, "ref"); | ||
| } | ||
| } | ||
|
|
||
| private class Property { | ||
| String name, value, ref; | ||
|
|
||
| public Property(JsonNode node) { | ||
| var item = node.isObject() && node.has("property") ? node.get("property") : node; | ||
|
|
||
| name = getTextContent(item, "name"); | ||
| value = getTextOrNull(item, "value"); | ||
| ref = getTextOrNull(item, "ref"); | ||
| } | ||
|
|
||
| public boolean isBlank() { | ||
| return name == null || name.isBlank(); | ||
| } | ||
| } | ||
|
|
||
| private List<ConstructorArg> parseConstructorArgList(JsonNode arr) { | ||
| if (arr == null || !arr.isArray()) return List.of(); | ||
|
|
||
| return StreamSupport.stream(arr.spliterator(), false) | ||
| .map(ConstructorArg::new) | ||
| .toList(); | ||
| } | ||
|
|
||
| private List<Property> parsePropertyList(JsonNode arr) { | ||
| if (arr == null || !arr.isArray()) return List.of(); | ||
|
|
||
| return StreamSupport.stream(arr.spliterator(), false) | ||
| .map(Property::new) | ||
| .toList(); | ||
| } | ||
|
christiangoerdes marked this conversation as resolved.
|
||
|
|
||
| private String getTextContent(JsonNode n, String key) { | ||
| JsonNode v = n.get(key); | ||
| if (v == null || !v.isTextual() || v.asText().isBlank()) | ||
| throw new IllegalArgumentException("Missing/blank '" + key + "' in bean spec."); | ||
| return v.asText(); | ||
| } | ||
|
|
||
| private String getTextOrNull(JsonNode n, String key) { | ||
| JsonNode v = n.get(key); | ||
| return v != null && v.isTextual() ? v.asText() : null; | ||
| } | ||
|
|
||
| private Object instantiate(Class<?> type, List<ConstructorArg> args) throws Exception { | ||
| int n = args == null ? 0 : args.size(); | ||
|
|
||
| Set<Constructor<?>> constructors = new LinkedHashSet<>(); | ||
| constructors.addAll(Arrays.asList(type.getConstructors())); | ||
| constructors.addAll(Arrays.asList(type.getDeclaredConstructors())); | ||
|
|
||
| Constructor<?> best = null; | ||
| Object[] bestArgs = null; | ||
|
|
||
| for (Constructor<?> c : constructors) { | ||
| if (c.getParameterCount() != n) continue; | ||
| Object[] resolved = tryResolveCtorArgs(c.getParameterTypes(), args); | ||
| if (resolved != null) { | ||
| best = c; | ||
| bestArgs = resolved; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (best == null) { | ||
| throw new IllegalArgumentException("No matching constructor found for %s with %d argument(s).".formatted(type.getName(), n)); | ||
| } | ||
|
|
||
| best.setAccessible(true); | ||
| return best.newInstance(bestArgs); | ||
| } | ||
|
|
||
| private Object[] tryResolveCtorArgs(Class<?>[] paramTypes, List<ConstructorArg> args) { | ||
| try { | ||
| Object[] resolved = new Object[paramTypes.length]; | ||
| for (int i = 0; i < paramTypes.length; i++) { | ||
| resolved[i] = resolveValueOrRef(paramTypes[i], args.get(i).value, args.get(i).ref); | ||
| } | ||
| return resolved; | ||
| } catch (Exception e) { | ||
| return null; // not compatible | ||
| } | ||
| } | ||
|
|
||
| private void applyProperties(Object target, List<Property> props) throws Exception { | ||
| for (Property p : props) { | ||
| if (p.isBlank()) | ||
| throw new IllegalArgumentException("Property name must not be blank."); | ||
|
|
||
| Method setter = findSetter(target.getClass(), p.name); | ||
| if (setter != null) { | ||
| Class<?> pt = setter.getParameterTypes()[0]; | ||
| setter.setAccessible(true); | ||
| setter.invoke(target, resolveValueOrRef(pt, p.value, p.ref)); | ||
| continue; | ||
| } | ||
|
|
||
| Field f = findField(target.getClass(), p.name); | ||
| if (f != null) { | ||
| f.setAccessible(true); | ||
| f.set(target, resolveValueOrRef(f.getType(), p.value, p.ref)); | ||
| continue; | ||
| } | ||
|
|
||
| throw new IllegalArgumentException("No setter/field found for property '%s' on %s".formatted(p.name, target.getClass().getName())); | ||
| } | ||
| } | ||
|
|
||
| private Method findSetter(Class<?> clazz, String prop) { | ||
| String setterName = getSetterName(prop); | ||
| for (Method method : clazz.getMethods()) { | ||
| if (matchesSetter(method, setterName)) return method; | ||
| } | ||
| for (Method method : clazz.getDeclaredMethods()) { | ||
| if (matchesSetter(method, setterName)) return method; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static boolean matchesSetter(Method method, String setterName) { | ||
| return method.getName().equals(setterName) && method.getParameterCount() == 1; | ||
| } | ||
|
|
||
| // e.g. bar -> setBar | ||
| private static @NotNull String getSetterName(String prop) { | ||
| if (prop == null || prop.isEmpty()) { | ||
| throw new IllegalArgumentException("Property name cannot be null or empty"); | ||
| } | ||
| return "set" + Character.toUpperCase(prop.charAt(0)) + prop.substring(1); | ||
| } | ||
|
christiangoerdes marked this conversation as resolved.
|
||
|
|
||
| private Field findField(Class<?> clazz, String name) { | ||
| Class<?> c = clazz; | ||
| while (c != null && c != Object.class) { | ||
| try { | ||
| return c.getDeclaredField(name); | ||
| } catch (NoSuchFieldException ignored) { | ||
| c = c.getSuperclass(); | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private Object resolveValueOrRef(Class<?> targetType, String value, String ref) { | ||
| if (ref != null && !ref.isBlank()) { | ||
| Object o = registry.resolveReference(ref); | ||
| if (o != null && !targetType.isInstance(o)) { | ||
| if (!(targetType.isPrimitive() && isWrapperOfPrimitive(targetType, o.getClass()))) { | ||
| throw new IllegalArgumentException("Ref '%s' is not assignable to %s".formatted(ref, targetType.getName())); | ||
| } | ||
| } | ||
| return o; | ||
| } | ||
| return ReflectionUtil.convert(value, targetType); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.