Skip to content
Merged
Show file tree
Hide file tree
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 Dec 11, 2025
5cdbd9f
Extend Bean class with attributes and properties, remove unnecessary …
christiangoerdes Dec 11, 2025
2f9d626
Validate setId methods and add missing top-level id property
christiangoerdes Dec 11, 2025
8972e3f
Enforce void return type for setId(String) methods and improve error …
christiangoerdes Dec 11, 2025
6463a94
Include child elements excludedFromJsonSchema when generating JSON Sc…
christiangoerdes Dec 11, 2025
d97651d
Introduce rootDef to MCElement and add root components support
christiangoerdes Dec 11, 2025
689d841
Support flow parser types and root defs in JSON schema generation; re…
christiangoerdes Dec 11, 2025
1fb6edf
Rename addTopLevelProperties to addRootLevelProperties, update method…
christiangoerdes Dec 12, 2025
9a52b1b
Add support for `allOf` in SchemaObject and adjust MCElement annotati…
christiangoerdes Dec 12, 2025
8edbc07
Mark Components as noEnvelope and temporarily disable envelope handli…
christiangoerdes Dec 12, 2025
e578ab1
rename topLevel to component
christiangoerdes Dec 12, 2025
8655dd5
rename topLevel to component
christiangoerdes Dec 12, 2025
c30fb10
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes Dec 12, 2025
055ce00
rename topLevel to component
christiangoerdes Dec 12, 2025
fa783c3
Merge branch 'master' into yaml-bean-support
christiangoerdes Dec 12, 2025
c183a44
rename rootDef to topLevel
christiangoerdes Dec 12, 2025
a531e0f
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes Dec 12, 2025
ff1c870
rename rootDef to topLevel
christiangoerdes Dec 12, 2025
256d97c
Generate component-only JSON Schema defs and reference them in compon…
christiangoerdes Dec 12, 2025
6e86af4
revert GenericYamlParser changes
christiangoerdes Dec 12, 2025
4dd0bd1
Add first version of using refs to reference component:
christiangoerdes Dec 12, 2025
8b0e7bb
Allow list items to be direct $ref component references
christiangoerdes Dec 12, 2025
a265cff
Generate patternProperties for components map and handle otherAttribu…
christiangoerdes Dec 12, 2025
7ac913f
Allow object-level $ref for components in JSON schema
christiangoerdes Dec 12, 2025
c7b2a5e
Remove unnecessary TODO comments and restore validation logic for @MC…
christiangoerdes Dec 12, 2025
a514d94
fixed YAMLParsingTests
rrayst Dec 12, 2025
e962dd0
generate the Components class automatically
rrayst Dec 12, 2025
80d4101
Merge branch 'master' into yaml-bean-support
christiangoerdes Dec 15, 2025
fe2fe39
Refactor `parseXML` and `parseYAML` logic to share context class load…
christiangoerdes Dec 15, 2025
6a45ebf
Enable `errorInListItemUniqueness` test and fix assertion message for…
christiangoerdes Dec 15, 2025
fbfdc4a
Add YAMLComponentsParsingTest, improve test utilities, and fix compon…
christiangoerdes Dec 15, 2025
5b215e5
Support YAML "components" and allow unordered bean assertions
christiangoerdes Dec 15, 2025
b8960d4
Validate collection element types and strengthen $ref handling with c…
christiangoerdes Dec 15, 2025
7e4c450
Refactor MethodSetter to improve collection handling and remove unuse…
christiangoerdes Dec 15, 2025
fded354
Remove unused `lazyInit` property and related methods in `Bean` class
christiangoerdes Dec 15, 2025
0d2b22b
Add test for component referencing another component in YAML parsing
christiangoerdes Dec 16, 2025
fcc854f
Merge branch 'master' into yaml-bean-support
christiangoerdes Dec 16, 2025
c4588e9
Prevent top-level elements from being used as nested children in JSON…
christiangoerdes Dec 16, 2025
75516f6
Remove component-only JSON Schema parsers and id setter validation, t…
christiangoerdes Dec 16, 2025
2949e01
Merge branch 'master' into yaml-bean-support
christiangoerdes Dec 16, 2025
c52e2ff
Refactor `JsonSchemaGenerator` to use constants for "components" and …
christiangoerdes Dec 16, 2025
5263072
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes Dec 16, 2025
6556e33
Add excludeFromFlow to @MCElement, mark interceptors excluded from fl…
christiangoerdes Dec 16, 2025
b3b6bc7
Refactor `$ref` handling in `GenericYamlParser` for improved error cl…
christiangoerdes Dec 16, 2025
8bdffd0
Remove unused `excludeFromFlow` set and `componentDefName` method in …
christiangoerdes Dec 16, 2025
363c25b
Add documentation and implement missing attributes in `Bean`, `Constr…
christiangoerdes Dec 16, 2025
674b4c7
Add BeanClassGenerator to produce Bean at compile time, invoke it fro…
christiangoerdes Dec 16, 2025
6790b2b
fix BeanClassGenerator
christiangoerdes Dec 16, 2025
0fa19ea
Add `BeanFactory` for dynamic bean instantiation and integrate protot…
christiangoerdes Dec 16, 2025
eb92ce4
Refactor `BeanRegistryImplementation` to improve error handling and s…
christiangoerdes Dec 16, 2025
25083cb
Add example for custom interceptor using YAML configuration
christiangoerdes Dec 16, 2025
84f8d1f
Refactor `BeanFactory` to improve class loading and enhance error ass…
christiangoerdes Dec 16, 2025
f97cc07
Update license header in `YAMLBeanParsingTest` for compliance with Ap…
christiangoerdes Dec 16, 2025
63405f2
coderabbit fixes
christiangoerdes Dec 16, 2025
bb238ab
Merge branch 'master' into yaml-bean-support
christiangoerdes Dec 17, 2025
e097e7c
Refactor `ComponentClassGenerator` and `BeanClassGenerator` to extend…
christiangoerdes Dec 17, 2025
8221115
Merge remote-tracking branch 'origin/yaml-bean-support' into yaml-bea…
christiangoerdes Dec 17, 2025
4e61efa
minor
christiangoerdes Dec 17, 2025
2d6a1f9
Update license headers for compliance and enhance bean reference supp…
christiangoerdes Dec 17, 2025
2d2459b
fix Parsing
christiangoerdes Dec 17, 2025
4509a02
refactor: simplify `BeanFactory` code and streamline property parsing
predic8 Dec 17, 2025
31f83ee
Refactor `BeanFactory` to simplify constructor argument parsing with …
christiangoerdes Dec 17, 2025
3b104ec
coderabbit suggestions
christiangoerdes Dec 17, 2025
c8867b3
refactor: improve code readability and modularity across `annot` package
predic8 Dec 17, 2025
11290b3
refactor: remove unused methods and make lists immutable in `annot` p…
predic8 Dec 17, 2025
2f19129
refactor: streamline code and improve readability in `annot` package
predic8 Dec 17, 2025
e24a672
feat: add ReflectionUtil with type conversion and primitive-wrapper u…
predic8 Dec 17, 2025
7edab1a
refactor: streamline `CompilerHelper` code and enhance modularity
predic8 Dec 17, 2025
f94438c
Refactor `YAMLBeanParsingTest` to split tests by scope, update scope …
christiangoerdes Dec 18, 2025
cf7eb0d
Add `Scope` enum, update exception type in `OtherAttributesInfo`, and…
christiangoerdes Dec 18, 2025
a31ddba
refactor: avoid NPE
rrayst Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@
* Allows the child to come from a schema other than Membrane core. Used for spring beans, e.g. ref to ssl bean
*/
boolean allowForeign() default false;

boolean excludeFromJson() default false; // excludes from JSON Schema (YAML)
}
12 changes: 11 additions & 1 deletion annot/src/main/java/com/predic8/membrane/annot/MCElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@

boolean mixed() default false;

/**
* Whether the element can be defined at the top-level of the config.
*/
boolean topLevel() default false;

/**
* Whether the element can be a separate bean in the XML schema, or a separate document in YAML/JSON.
*/
boolean topLevel() default true;
boolean component() default true;

String configPackage() default "";

Expand All @@ -54,4 +59,9 @@
* This does not have any effect on the XML grammar.
*/
boolean noEnvelope() default false;

/**
* Whether the element should be configurable as part of the interceptor flow
*/
boolean excludeFromFlow() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,10 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment

scan(main, ii);

if (ii.getAnnotation().topLevel())
main.getTopLevels().put(ii.getAnnotation().name(), ii);
if (ii.getAnnotation().component())
main.getComponents().put(ii.getAnnotation().name(), ii);

if (ii.getAnnotation().noEnvelope()) {
if (ii.getAnnotation().topLevel())
throw new ProcessingException("@MCElement(..., noEnvelope=true, topLevel=true) is invalid.", ii.getElement());
if (ii.getAnnotation().mixed())
throw new ProcessingException("@MCElement(..., noEnvelope=true, mixed=true) is invalid.", ii.getElement());
if (ii.getChildElementSpecs().size() != 1)
Expand Down Expand Up @@ -254,8 +252,8 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
for (Entry<TypeElement, ElementInfo> e : main.getElements().entrySet()) {
if (!processingEnv.getTypeUtils().isAssignable(e.getKey().asType(), f.getKey().asType()))
continue;
if (targetIsObject && !isTopLevelMCElement(e.getKey()))
continue; // only allow topLevel MCElements for Object
if (targetIsObject && !isComponent(e.getKey()))
continue; // only allow component MCElements for Object
cedi.getElementInfo().add(e.getValue());
}
}
Expand All @@ -272,9 +270,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment

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

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

private boolean isTopLevelMCElement(TypeElement type) {
private boolean isComponent(TypeElement type) {
MCElement mcElement = type.getAnnotation(MCElement.class);
return (mcElement != null) && mcElement.topLevel();
return (mcElement != null) && mcElement.component();
}

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

public void process(Model m) throws IOException {
if (new ComponentClassGenerator(processingEnv).writeJava(m))
return; // we will be called again to handle the newly generated class.
if (new BeanClassGenerator(processingEnv).writeJava(m))
return; // we will be called again to handle the newly generated class.
new Schemas(processingEnv).writeXSD(m);
new KubernetesBootstrapper(processingEnv).boot(m);
new JsonSchemaGenerator(processingEnv).write(m);
Expand Down
252 changes: 252 additions & 0 deletions annot/src/main/java/com/predic8/membrane/annot/bean/BeanFactory.java
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();
Comment thread
christiangoerdes marked this conversation as resolved.
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();
}
Comment thread
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);
}
Comment thread
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);
}
}
Loading
Loading