diff --git a/annot/src/main/java/com/predic8/membrane/annot/MCChildElement.java b/annot/src/main/java/com/predic8/membrane/annot/MCChildElement.java index 8f95eb513a..7bcdc44e14 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/MCChildElement.java +++ b/annot/src/main/java/com/predic8/membrane/annot/MCChildElement.java @@ -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) } diff --git a/annot/src/main/java/com/predic8/membrane/annot/MCElement.java b/annot/src/main/java/com/predic8/membrane/annot/MCElement.java index ed8b6ff4f2..cb3cff1218 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/MCElement.java +++ b/annot/src/main/java/com/predic8/membrane/annot/MCElement.java @@ -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 ""; @@ -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; } diff --git a/annot/src/main/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessor.java b/annot/src/main/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessor.java index 4aa3e3964d..3ff9059fc4 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessor.java +++ b/annot/src/main/java/com/predic8/membrane/annot/SpringConfigurationXSDGeneratingAnnotationProcessor.java @@ -215,12 +215,10 @@ public boolean process(Set 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) @@ -254,8 +252,8 @@ public boolean process(Set annotations, RoundEnvironment for (Entry 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()); } } @@ -272,9 +270,9 @@ public boolean process(Set annotations, RoundEnvironment for (MainInfo main : m.getMains()) { for (Entry 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 uniquenessErrors = getUniquenessError(f.getValue(), main); if (!uniquenessErrors.isEmpty()) @@ -299,9 +297,9 @@ public boolean process(Set 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 getUniquenessError(ElementInfo ii, MainInfo main) { @@ -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); diff --git a/annot/src/main/java/com/predic8/membrane/annot/bean/BeanFactory.java b/annot/src/main/java/com/predic8/membrane/annot/bean/BeanFactory.java new file mode 100644 index 0000000000..0a4d50e928 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/bean/BeanFactory.java @@ -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 parseConstructorArgList(JsonNode arr) { + if (arr == null || !arr.isArray()) return List.of(); + + return StreamSupport.stream(arr.spliterator(), false) + .map(ConstructorArg::new) + .toList(); + } + + private List parsePropertyList(JsonNode arr) { + if (arr == null || !arr.isArray()) return List.of(); + + return StreamSupport.stream(arr.spliterator(), false) + .map(Property::new) + .toList(); + } + + 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 args) throws Exception { + int n = args == null ? 0 : args.size(); + + Set> 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 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 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); + } + + 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); + } +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/BeanClassGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/BeanClassGenerator.java new file mode 100644 index 0000000000..75d9ddaa60 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/BeanClassGenerator.java @@ -0,0 +1,165 @@ +/* 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.generator; + +import javax.annotation.processing.ProcessingEnvironment; + +public class BeanClassGenerator extends ClassGenerator{ + + public BeanClassGenerator(ProcessingEnvironment processingEnv) { + super(processingEnv); + } + + @Override + protected String getClassName() { + return "Bean"; + } + + @Override + protected String getClassImpl() { + return """ + import com.predic8.membrane.annot.MCAttribute; + import com.predic8.membrane.annot.MCChildElement; + import com.predic8.membrane.annot.MCElement; + import com.predic8.membrane.annot.generator.Scope; + + import java.util.ArrayList; + import java.util.List; + + /** + * Spring-like bean definition usable in YAML components: + * components: + * : + * bean: + * class: com.example.MyInterceptor + * scope: SINGLETON + * constructorArgs: + * - constructorArg: + * value: foo + * properties: + * - property: + * name: bar + * value: baz + */ + @MCElement(name = "bean") + public class Bean { + + private String className; + private Scope scope = Scope.SINGLETON; + private List constructorArgs = new ArrayList<>(); + private List properties = new ArrayList<>(); + + @MCAttribute + public void setClass(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } + + @MCAttribute + public void setScope(Scope scope) { + this.scope = scope; + } + + public Scope getScope() { + return scope; + } + + @MCChildElement(order = 3) + public void setConstructorArgs(List constructorArgs) { + this.constructorArgs = constructorArgs; + } + + public List getConstructorArgs() { + return constructorArgs; + } + + @MCChildElement + public void setProperties(List properties) { + this.properties = properties; + } + + public List getProperties() { + return properties; + } + + public enum Scope { + SINGLETON, + PROTOTYPE + } + + @MCElement(name = "constructorArg", component = false) + public static class ConstructorArg { + private String value; + private String ref; + + @MCAttribute + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @MCAttribute + public void setRef(String ref) { + this.ref = ref; + } + + public String getRef() { + return ref; + } + } + + @MCElement(name = "property", component = false) + public static class Property { + private String name; + private String value; + private String ref; + + @MCAttribute + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @MCAttribute + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @MCAttribute + public void setRef(String ref) { + this.ref = ref; + } + + public String getRef() { + return ref; + } + } + } + """; + } +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/BlueprintParsers.java b/annot/src/main/java/com/predic8/membrane/annot/generator/BlueprintParsers.java index b8003a3ce5..dbf8dcb855 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/BlueprintParsers.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/BlueprintParsers.java @@ -78,7 +78,7 @@ public void writeParserDefinitior(Model m) throws IOException { "\r\n" + " public void init() {\r\n"); for (ElementInfo i : main.getIis()) { - if (i.getAnnotation().topLevel()) { + if (i.getAnnotation().component()) { bw.write(" registerGlobalBeanDefinitionParser(\"" + i.getAnnotation().name() + "\", new " + i.getParserClassSimpleName() + "());\r\n"); } else { for (ChildElementDeclarationInfo cedi : i.getUsedBy()) { diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/ClassGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/ClassGenerator.java new file mode 100644 index 0000000000..6c5e91fa90 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/ClassGenerator.java @@ -0,0 +1,72 @@ +package com.predic8.membrane.annot.generator; + +import com.predic8.membrane.annot.model.MainInfo; +import com.predic8.membrane.annot.model.Model; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.processing.FilerException; +import javax.annotation.processing.ProcessingEnvironment; +import java.io.IOException; +import java.io.Writer; + +public abstract class ClassGenerator { + + public static final String COPYRIGHT = """ + /* 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. */ + + """; + + private final ProcessingEnvironment processingEnv; + + public ClassGenerator(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + /** + * @return true if the file was written, false if it already existed + */ + public boolean writeJava(Model m) throws IOException { + for (MainInfo main : m.getMains()) { + try { + try (Writer w = processingEnv.getFiler().createSourceFile(getFileName(main)).openWriter()) { + w.write(COPYRIGHT); + w.write(getPackage(main)); + w.write(getClassImpl()); + } + return true; + } catch (FilerException e) { + String msg = e.getMessage(); + if (msg != null && (msg.contains("Source file already created") + || msg.contains("Attempt to recreate a file for"))) { + return false; + } + throw e; + } + } + return false; + } + + private @NotNull String getFileName(MainInfo main) { + return main.getAnnotation().outputPackage() + "." + getClassName(); + } + + private static @NotNull String getPackage(MainInfo main) { + return "package " + main.getAnnotation().outputPackage() + ";\r\n"; + } + + protected abstract String getClassName(); + + protected abstract String getClassImpl(); +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/ComponentClassGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/ComponentClassGenerator.java new file mode 100644 index 0000000000..c78fd3a9ba --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/ComponentClassGenerator.java @@ -0,0 +1,58 @@ +/* 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.generator; + +import javax.annotation.processing.ProcessingEnvironment; + +public class ComponentClassGenerator extends ClassGenerator{ + + + public ComponentClassGenerator(ProcessingEnvironment processingEnv) { + super(processingEnv); + } + + @Override + protected String getClassName() { + return "Components"; + } + + @Override + protected String getClassImpl() { + return """ + import com.predic8.membrane.annot.MCElement; + import com.predic8.membrane.annot.MCOtherAttributes; + + import java.util.Map; + + @MCElement(name = "components", topLevel = true) + public class Components { + + Map components; + + public Map getComponents() { + return components; + } + + @MCOtherAttributes + public void setComponents(Map components) { + if (this.components == null) + this.components = new java.util.LinkedHashMap<>(); + if (components != null) + this.components.putAll(components); + } + } + """; + } +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/HelpReference.java b/annot/src/main/java/com/predic8/membrane/annot/generator/HelpReference.java index 6f12ba6aca..deb23e96b2 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/HelpReference.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/HelpReference.java @@ -121,10 +121,10 @@ private void handle(Model m, MainInfo main, ElementInfo ei) throws XMLStreamExce xew.writeAttribute("name", ei.getAnnotation().name()); if (ei.getAnnotation().mixed()) xew.writeAttribute("mixed", "true"); - xew.writeAttribute("topLevel", Boolean.toString(ei.getAnnotation().topLevel())); + xew.writeAttribute("component", Boolean.toString(ei.getAnnotation().component())); xew.writeAttribute("id", ei.getId()); xew.writeAttribute("deprecated", Boolean.toString(ei.isDeprecated())); - if (!ei.getAnnotation().topLevel()) { + if (!ei.getAnnotation().component()) { String primaryParentId = getPrimaryParentId(m, main, ei); if (primaryParentId != null) xew.writeAttribute("primaryParentId", primaryParentId); @@ -174,7 +174,7 @@ private String getPrimaryParentId(Model m, MainInfo mi, ElementInfo ei) { } } for (ElementInfo ei2 : possibleParents) - if (ei2.getAnnotation().topLevel()) + if (ei2.getAnnotation().component()) return ei2.getId(); possibleParents.remove(ei); if (possibleParents.size() > 0) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 34b9b3a090..9fad57fcc0 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -19,12 +19,14 @@ import com.predic8.membrane.annot.generator.kubernetes.model.*; import com.predic8.membrane.annot.model.*; import com.predic8.membrane.annot.model.doc.*; +import org.jetbrains.annotations.*; import javax.annotation.processing.*; import javax.lang.model.element.*; import javax.tools.*; import java.io.*; import java.util.*; +import java.util.stream.*; import static com.predic8.membrane.annot.generator.kubernetes.model.SchemaFactory.*; import static com.predic8.membrane.annot.generator.util.SchemaGeneratorUtil.*; @@ -40,32 +42,19 @@ public class JsonSchemaGenerator extends AbstractGrammar { public static final String MEMBRANE_SCHEMA_JSON_FILENAME = "membrane.schema.json"; + public static final String COMPONENTS = "components"; - private final Map topLevelAdded = new HashMap<>(); + // TODO keep this pattern or allow *? + public static final String COMPONENT_ID_PATTERN = "^[A-Za-z_][A-Za-z0-9_-]*$"; - public JsonSchemaGenerator(ProcessingEnvironment processingEnv) { - super(processingEnv); - } + private final Map componentAdded = new HashMap<>(); private boolean flowDefCreated = false; private Schema schema; - private static final Set excludeFromFlow = Set.of( - "httpClient", - "ruleMatching", - "wadlRewriter", - "global", - "exchangeStore", - "accountRegistration", - "userFeature", - "tcp", - "wsaEndpointRewriter", - "flowInitiator", - "kubernetesValidation", - "dispatching", - "groovyTemplate", - "adminApi" - ); + public JsonSchemaGenerator(ProcessingEnvironment processingEnv) { + super(processingEnv); + } public void write(Model m) throws IOException { for (MainInfo main : m.getMains()) { @@ -77,7 +66,7 @@ private void assemble(Model m, MainInfo main) throws IOException { // Reset so multiple calls would be possible flowDefCreated = false; schema = schema("membrane"); - topLevelAdded.clear(); + componentAdded.clear(); addParserDefinitions(m, main); addTopLevelProperties(m, main); @@ -87,26 +76,27 @@ private void assemble(Model m, MainInfo main) throws IOException { private void addTopLevelProperties(Model m, MainInfo main) { schema.additionalProperties(false); - List> kinds = new ArrayList<>(); + List> kinds = main.getElements().values().stream() + .filter(e -> e.getAnnotation().topLevel()) + .map(e -> createTopLevelProperty(e, m)) + .collect(Collectors.toUnmodifiableList()); - main.getElements().values().forEach(e -> { - if (!e.getAnnotation().topLevel()) - return; + if (!kinds.isEmpty()) + schema.oneOf(kinds); + } - String name = e.getAnnotation().name(); - String refName = "#/$defs/" + e.getXSDTypeName(m); + private AbstractSchema createTopLevelProperty(ElementInfo e, Model m) { - schema.property(ref(name).ref(refName)); + String name = e.getAnnotation().name(); + String refName = "#/$defs/" + e.getXSDTypeName(m); - kinds.add(object() - .additionalProperties(false) - .property(ref(name) - .ref(refName) - .required(true))); - }); + schema.property(ref(name).ref(refName)); - if (!kinds.isEmpty()) - schema.oneOf(kinds); + return object() + .additionalProperties(false) + .property(ref(name) + .ref(refName) + .required(true)); } private void addParserDefinitions(Model m, MainInfo main) { @@ -126,30 +116,50 @@ private void addParserDefinitions(Model m, MainInfo main) { private SchemaObject createParser(Model m, MainInfo main, ElementInfo elementInfo) { String parserName = elementInfo.getXSDTypeName(m); + if (isComponentsMap(elementInfo)) { + return createComponentsMapParser(m, main, elementInfo, parserName); + } + // e.g. to prevent a request from needing a flow child noEnvelope=true is used if (elementInfo.getAnnotation().noEnvelope()) { // With noEnvelope=true, there should be exactly one child element - ChildElementInfo child = elementInfo.getChildElementSpecs().getFirst(); var childName = child.getPropertyName(); - if (!topLevelAdded.containsKey(childName) && !shouldGenerateParserType(child)) { + if (!componentAdded.containsKey(childName) && !shouldGenerateFlowParserType(child)) { SchemaArray array = array(childName + "Parser"); processMCChilds(m, main, child.getEi(), array); schema.definition(array); - topLevelAdded.put(childName, true); + componentAdded.put(childName, true); } return ref(parserName).ref("#/$defs/%sParser".formatted(childName)); } - SchemaObject parser = object(parserName) - .additionalProperties(elementInfo.getOai() != null) - .description(getDescriptionContent(elementInfo)); + SchemaObject parser = getParserSchemaObject(elementInfo, parserName); + collectProperties(m, main, elementInfo, parser); + + // Allow object-level component reference if any setter expects a component. + if (hasComponentChild(elementInfo, main) && !parser.hasProperty("$ref")) { + parser.property(string("$ref") + .description("JSON Pointer to a component.") + .required(false)); + } + return parser; } + private SchemaObject getParserSchemaObject(ElementInfo elementInfo, String parserName) { + return object(parserName) + .additionalProperties( elementInfo.isString()) + .description(getDescriptionContent(elementInfo)); + } + + private boolean isComponentsMap(ElementInfo ei) { + return COMPONENTS.equals(ei.getAnnotation().name()) && ei.isObject(); + } + private String getDescriptionContent(AbstractJavadocedInfo elementInfo) { Doc doc = elementInfo.getDoc(processingEnv); if (doc == null) { @@ -167,12 +177,16 @@ private FileObject createFile(MainInfo main) throws IOException { return processingEnv.getFiler() .createResource( CLASS_OUTPUT, - main.getAnnotation().outputPackage().replaceAll("\\.spring$", ".json"), + getOutputPackage(main), MEMBRANE_SCHEMA_JSON_FILENAME, sources.toArray(new Element[0]) ); } + private static @NotNull String getOutputPackage(MainInfo main) { + return main.getAnnotation().outputPackage().replaceAll("\\.spring$", ".json"); + } + private void processMCAttributes(ElementInfo i, SchemaObject so) { i.getAis().forEach(ai -> { @@ -180,10 +194,6 @@ private void processMCAttributes(ElementInfo i, SchemaObject so) { if (ai.excludedFromJsonSchema()) return; - // hide id only on top-level elements - if ("id".equals(ai.getXMLName()) && i.getAnnotation().topLevel()) { - return; - } so.property(createProperty(ai)); }); } @@ -221,34 +231,47 @@ private void collectTextContent(ElementInfo i, SchemaObject so) { private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSchema so) { for (ChildElementInfo cei : i.getChildElementSpecs()) { - AbstractSchema parent2 = so; - if (cei.isList()) { - if (shouldGenerateParserType(cei)) { - var sos = new ArrayList(); - for (ElementInfo ei : main.getChildElementDeclarations().get(cei.getTypeDeclaration()).getElementInfo()) { - if (excludeFromFlow.contains(ei.getAnnotation().name())) - continue; - sos.add(object() - .additionalProperties(false) - .property(ref(ei.getAnnotation().name()).ref("#/$defs/" + ei.getXSDTypeName(m)))); - } - processList(i, so, cei, sos); + if (shouldGenerateFlowParserType(cei)) { + processList(i, so, cei, getSchemaObjects(m, main, cei)); continue; } parent2 = processList(i, so, cei, null); - } else { - // Check if we need a $ref or if it is allowed everywhere - if (cei.getAnnotation().allowForeign()) { - // parent2.addProperty(new SchemaObject("$ref").attribute("type", "string")); - } } - addChildsAsProperties(m, main, cei, (SchemaObject) parent2); + addChildsAsProperties(m, main, cei, (SchemaObject) parent2, isComponentsList(i, cei), cei.isList()); + } + } + + private static @NotNull ArrayList getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) { + var sos = new ArrayList(); + + for (ElementInfo ei : main.getChildElementDeclarations().get(cei.getTypeDeclaration()).getElementInfo()) { + if (ei.getAnnotation().excludeFromFlow()) + continue; + + sos.add(object() + .additionalProperties(false) + .property(ref(ei.getAnnotation().name()) + .ref("#/$defs/" + ei.getXSDTypeName(m)) + .required(true))); } + // Allow referencing a component instance directly on list-item level: + // flow: + // - $ref: ... + sos.add(object() + .additionalProperties(false) + .property( string("$ref").required(true))); + return sos; } - private boolean shouldGenerateParserType(ChildElementInfo cei) { + private boolean isComponentsList(ElementInfo parent, ChildElementInfo cei) { + return COMPONENTS.equals(parent.getAnnotation().name()) + && parent.getAnnotation().noEnvelope() + && COMPONENTS.equals(cei.getPropertyName()); + } + + private boolean shouldGenerateFlowParserType(ChildElementInfo cei) { return "flow".equals(cei.getPropertyName()) && !isFlowFromWebSocket(cei); } @@ -260,7 +283,7 @@ boolean isFlowFromWebSocket(ChildElementInfo cei) { private AbstractSchema processList(ElementInfo i, AbstractSchema so, ChildElementInfo cei, ArrayList sos) { SchemaObject items = object("items"); - if (shouldGenerateParserType(cei)) { + if (shouldGenerateFlowParserType(cei)) { addFlowParserRef(so, sos); return items; } @@ -291,15 +314,31 @@ private void addFlowParserRef(AbstractSchema so, List sos) { } } - private void addChildsAsProperties(Model m, MainInfo main, ChildElementInfo cei, SchemaObject parent2) { - for (ElementInfo ei : getChildElementDeclarationInfo(main, cei).getElementInfo()) { - parent2.property(ref(ei.getAnnotation().name()) - .ref("#/$defs/" + ei.getXSDTypeName(m))) + private void addChildsAsProperties(Model m, MainInfo main, ChildElementInfo cei, SchemaObject parent2, boolean componentsContext, boolean listItemContext) { + var eis = getChildElementDeclarationInfo(main, cei).getElementInfo().stream() + // Top-level elements cannot be configurable as nested children + .filter(ei -> !ei.getAnnotation().topLevel()) + .toList(); + + // Generic list-item reference support: + // If this list can contain at least one @MCElement(component=true) type, + // allow "- $ref: ..." as an alternative list item shape. + if (listItemContext && !componentsContext && eis.stream().anyMatch(ei -> ei.getAnnotation().component())) { + parent2.property(string("$ref").required(false)); + } + + for (ElementInfo ei : eis) { + + parent2.property(getRef(m, ei)) .description(getDescriptionContent(ei)) .required(cei.isRequired()); } } + private static SchemaRef getRef(Model m, ElementInfo ei) { + return ref(ei.getAnnotation().name()).ref("#/$defs/" + ei.getXSDTypeName(m)); + } + private static ChildElementDeclarationInfo getChildElementDeclarationInfo(MainInfo main, ChildElementInfo cei) { return getChildElementDeclarations(main).get(cei.getTypeDeclaration()); } @@ -326,12 +365,50 @@ private void writeSchema(MainInfo main, Schema schema) throws IOException { } } + private SchemaObject createComponentsMapParser(Model m, MainInfo main, ElementInfo elementInfo, String parserName) { + SchemaObject parser = object(parserName) + .additionalProperties(false) // only IDs via patternProperties + .description(getDescriptionContent(elementInfo)); + parser.patternProperty(COMPONENT_ID_PATTERN, anyOf(getComponents(m, main))); + return parser; + } + + private static @NotNull ArrayList getComponents(Model m, MainInfo main) { + var variants = new ArrayList(); + + for (ElementInfo comp : main.getElements().values()) { + if (!comp.getAnnotation().component()) + continue; + + if (comp.getAnnotation().topLevel()) + continue; + + variants.add(object() + .additionalProperties(false) + .property(ref(comp.getAnnotation().name()) + .ref("#/$defs/" + comp.getXSDTypeName(m)) + .required(true))); + } + return variants; + } + + private boolean hasComponentChild(ElementInfo parent, MainInfo main) { + for (ChildElementInfo cei : parent.getChildElementSpecs()) { + var decl = getChildElementDeclarationInfo(main, cei); + if (decl == null) continue; + + if (decl.getElementInfo().stream().anyMatch(ei -> ei.getAnnotation().component())) + return true; + } + return false; + } + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsText(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("<[^>]+>", "").replaceAll("\\s+", " ").trim()); } - // For description. Probably we'll include that later. (Temporarily deactivated! + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsHtml(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("\\s+", " ").trim()); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/Parsers.java b/annot/src/main/java/com/predic8/membrane/annot/generator/Parsers.java index 553d70f772..e9cf87c345 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/Parsers.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/Parsers.java @@ -67,7 +67,7 @@ public void writeParserDefinitior(Model m) throws IOException { "\r\n" + " public static void registerBeanDefinitionParsers(NamespaceHandler nh) {\r\n"); for (ElementInfo i : main.getIis()) { - if (i.getAnnotation().topLevel()) { + if (i.getAnnotation().component()) { bw.write(" nh.registerGlobalBeanDefinitionParser(\"" + i.getAnnotation().name() + "\", new " + i.getParserClassSimpleName() + "());\r\n"); } else { for (ChildElementDeclarationInfo cedi : i.getUsedBy()) { diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/Schemas.java b/annot/src/main/java/com/predic8/membrane/annot/generator/Schemas.java index 66c4af1c38..b0de70fc48 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/Schemas.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/Schemas.java @@ -91,7 +91,7 @@ private void assembleDeclarations(Writer w, Model m, MainInfo main) throws Proce private void assembleElementDeclaration(Writer w, Model m, MainInfo main, ElementInfo i) throws ProcessingException, IOException { String footer; - if (i.getAnnotation().topLevel()) { + if (i.getAnnotation().component()) { w.append("\r\n"); assembleDocumentation(w, i); w.append("\r\n"); @@ -126,7 +126,7 @@ private void assembleElementInfo(Writer w, Model m, MainInfo main, ElementInfo i w.append("\r\n"); assembleDocumentation(w, cei); for (ElementInfo ei : main.getChildElementDeclarations().get(cei.getTypeDeclaration()).getElementInfo()) { - if (ei.getAnnotation().topLevel()) + if (ei.getAnnotation().component()) w.append("\r\n"); else w.append("\r\n"); diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java b/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java new file mode 100644 index 0000000000..d66ec77a73 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java @@ -0,0 +1,6 @@ +package com.predic8.membrane.annot.generator; + +public enum Scope { + SINGLETON, + PROTOTYPE +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/AbstractGrammar.java b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/AbstractGrammar.java index 0c31bdc241..e12db0d8a6 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/AbstractGrammar.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/AbstractGrammar.java @@ -62,13 +62,13 @@ protected WritableNames getElementNames(ElementInfo ei) { return new WritableNames(ei); } - protected Stream getTopLevelStream(MainInfo main) { + protected Stream getComponentStream(MainInfo main) { return main.getElements().values().stream() - .filter(ei -> ei.getAnnotation().topLevel()); + .filter(ei -> ei.getAnnotation().component()); } - protected List getTopLevelElementInfos(MainInfo main) { - return getTopLevelStream(main) + protected List getComponentElementInfos(MainInfo main) { + return getComponentStream(main) .collect(Collectors.toList()); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/Grammar.java b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/Grammar.java index 8f953170c3..e3adfb4a49 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/Grammar.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/Grammar.java @@ -14,14 +14,17 @@ package com.predic8.membrane.annot.generator.kubernetes; import com.predic8.membrane.annot.model.*; +import org.jetbrains.annotations.*; import javax.annotation.processing.*; import javax.lang.model.element.*; import javax.tools.*; import java.io.*; import java.util.*; +import java.util.function.*; import java.util.stream.*; +import static com.predic8.membrane.annot.generator.ClassGenerator.*; import static java.util.stream.Stream.*; /** @@ -39,17 +42,12 @@ protected String fileName() { } @Override - protected void write(Model m) throws IOException{ + protected void write(Model m) throws IOException { m.getMains().forEach(main -> { try { List sources = new ArrayList<>(main.getInterceptorElements()); sources.add(main.getElement()); - - FileObject fileObject = processingEnv.getFiler().createSourceFile( - main.getAnnotation().outputPackage() + "." + fileName(), - sources.toArray(new Element[0])); - - try (BufferedWriter w = new BufferedWriter(fileObject.openWriter())) { + try (BufferedWriter w = new BufferedWriter(getSourceFile(main, sources).openWriter())) { assemble(w, main); } } catch (IOException e) { @@ -58,84 +56,75 @@ protected void write(Model m) throws IOException{ }); } + private JavaFileObject getSourceFile(MainInfo main, List sources) throws IOException { + return processingEnv.getFiler().createSourceFile( + main.getAnnotation().outputPackage() + "." + fileName(), + sources.toArray(new Element[0])); + } + private void assemble(Writer w, MainInfo main) throws IOException { writeCopyright(w); writeClassContent(w, main); } private void writeCopyright(Writer w) throws IOException { - appendLine(w, - "/* Copyright 2021 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/license/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.", - "*/" - ); + appendLine(w, COPYRIGHT); } private void writeClassContent(Writer w, MainInfo mainInfo) throws IOException { w.write(""" - package %s; - - import java.util.Map; - import java.util.List; - import java.util.HashMap; - import java.util.ArrayList; - import com.predic8.membrane.annot.Grammar; - - /** - * Automatically generated by {@link %s} - */ - public class %s implements Grammar { - private static Map> elementMapping = new HashMap<>(); - private static Map>> localElementMapping = new HashMap<>(); - private static List crdSingularNames = new ArrayList<>(); - - private static void localElementMappingPut(String context, String name, Class clazz) { - Map> local = localElementMapping.get(context); - if (local == null) { - local = new HashMap<>(); - localElementMapping.put(context, local); - localElementMapping.put(context.toLowerCase(), local); + package %s; + + import java.util.Map; + import java.util.List; + import java.util.HashMap; + import java.util.ArrayList; + import com.predic8.membrane.annot.Grammar; + + /** + * Automatically generated by {@link %s} + */ + public class %s implements Grammar { + private static Map> elementMapping = new HashMap<>(); + private static Map>> localElementMapping = new HashMap<>(); + private static List crdSingularNames = new ArrayList<>(); + + private static void localElementMappingPut(String context, String name, Class clazz) { + Map> local = localElementMapping.get(context); + if (local == null) { + local = new HashMap<>(); + localElementMapping.put(context, local); + localElementMapping.put(context.toLowerCase(), local); + } + local.put(name, clazz); + local.put(name.toLowerCase(), clazz); } - local.put(name, clazz); - local.put(name.toLowerCase(), clazz); - } - - @Override - public Class getLocal(String context, String key) { - Map> local = localElementMapping.get(context); - if (local == null) - return null; - return local.get(key); - } - - @Override - public Class getElement(String key) { - return elementMapping.get(key); - } - - @Override - public List getCrdSingularNames() { - return crdSingularNames; - } - - @Override - public String getSchemaLocation() { - return "classpath:/%s/membrane.schema.json"; - } - - static { - """.formatted( + + @Override + public Class getLocal(String context, String key) { + Map> local = localElementMapping.get(context); + if (local == null) + return null; + return local.get(key); + } + + @Override + public Class getElement(String key) { + return elementMapping.get(key); + } + + @Override + public List getCrdSingularNames() { + return crdSingularNames; + } + + @Override + public String getSchemaLocation() { + return "classpath:/%s/membrane.schema.json"; + } + + static { + """.formatted( mainInfo.getAnnotation().outputPackage(), Grammar.class.getName(), fileName(), @@ -148,60 +137,54 @@ public String getSchemaLocation() { w.write(assembleElementMapping(mainInfo) + "\n"); w.write(""" + } } - } - """); + """); } private String assembleElementMapping(MainInfo main) { return concat( // global main.getIis().stream() - .filter(ei -> ei.getAnnotation().topLevel()) - .map(ei -> String.format(" elementMapping.put(\"%s\", %s.class);" + System.lineSeparator() - + " elementMapping.put(\"%s\", %s.class);", - ei.getAnnotation().name(), - ei.getElement().getQualifiedName(), - ei.getAnnotation().name().toLowerCase(), - ei.getElement().getQualifiedName())), + .filter(ei -> ei.getAnnotation().component()) + .map(generateElementMappingString()), // non-global main.getIis().stream() .flatMap(ei -> ei.getChildElementSpecs().stream().map(cei -> Pair.of(ei, cei))) .flatMap(p -> main.getChildElementDeclarations().get(p.y.getTypeDeclaration()) .getElementInfo().stream().map(ei -> Pair.of(p.x, ei))) - .filter(p -> !p.y.getAnnotation().topLevel()) - .map(p -> String.format(" localElementMappingPut(\"%s\", \"%s\", %s.class);", - p.x.getAnnotation().name(), - p.y.getAnnotation().name(), - p.y.getElement().getQualifiedName()))) + .filter(p -> !p.y.getAnnotation().component()) + .map(generateLocalElementMapping())) .collect(Collectors.joining(System.lineSeparator())); } + private static @NotNull Function, String> generateLocalElementMapping() { + return p -> String.format(" localElementMappingPut(\"%s\", \"%s\", %s.class);", + p.x.getAnnotation().name(), + p.y.getAnnotation().name(), + p.y.getElement().getQualifiedName()); + } + + private static @NotNull Function generateElementMappingString() { + return ei -> String.format(" elementMapping.put(\"%s\", %s.class);" + System.lineSeparator() + + " elementMapping.put(\"%s\", %s.class);", + ei.getAnnotation().name(), + ei.getElement().getQualifiedName(), + ei.getAnnotation().name().toLowerCase(), + ei.getElement().getQualifiedName()); + } + private String assembleCrdSingularNames(MainInfo main) { - return getTopLevelStream(main) + return getComponentStream(main) .map(ei -> ei.getAnnotation().name().toLowerCase()) .map(s -> " crdSingularNames.add(\"" + s + "\");") .collect(Collectors.joining(System.lineSeparator())); } - private static class Pair { - X x; - Y y; - public Pair(X x, Y y) { - this.x = x; - this.y = y; - } - + // TODO Merge with Pair record of core + private record Pair(X x, Y y) { public static Pair of(X x, Y y) { return new Pair<>(x, y); } - - public X getX() { - return x; - } - - public Y getY() { - return y; - } } } diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sJsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sJsonSchemaGenerator.java index 2ebd67b0eb..e35e2bad81 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sJsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sJsonSchemaGenerator.java @@ -45,7 +45,7 @@ public void write(Model m) throws IOException { } private void assemble(Model m, MainInfo main) throws IOException { - for (ElementInfo elementInfo : getTopLevelElementInfos(main)) { + for (ElementInfo elementInfo : getComponentElementInfos(main)) { String name = elementInfo.getAnnotation().name().toLowerCase(); FileObject fo = createFileObject(main, name + ".schema.json"); try (BufferedWriter w = new BufferedWriter(fo.openWriter())) { @@ -140,20 +140,28 @@ private void collectChildElements(Model m, MainInfo main, ElementInfo i, Abstrac if (isList) { SchemaObject items = object("items").additionalProperties(cei.getAnnotation().allowForeign()); - if (i.getAnnotation().noEnvelope()) { - if (so instanceof SchemaArray sa) - sa.items(items); - else - throw new ProcessingException("@MCElement(noEnvelope=true) is not an array. Implementation error?", i.getElement()); - } else { - if (so instanceof SchemaObject sObj) { - SchemaArray array = array(cei.getPropertyName()); - array.items(items); - sObj.property(array.required(cei.isRequired())); - } else { - throw new ProcessingException("@MCElement(noEnvelope=false) is not an object. Implementation error?", i.getElement()); - } - } + //TODO fix: the 'components' require this structure: +// components: +// - bean: +// ... +// - xmlProtection: +// ... + + +// if (i.getAnnotation().noEnvelope()) { +// if (so instanceof SchemaArray sa) +// sa.items(items); +// else +// throw new ProcessingException("@MCElement(noEnvelope=true) is not an array. Implementation error?", i.getElement()); +// } else { +// if (so instanceof SchemaObject sObj) { +// SchemaArray array = array(cei.getPropertyName()); +// array.items(items); +// sObj.property(array.required(cei.isRequired())); +// } else { +// throw new ProcessingException("@MCElement(noEnvelope=false) is not an object. Implementation error?", i.getElement()); +// } +// } parent2 = items; } else { if (cei.getAnnotation().allowForeign()) { diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sYamlGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sYamlGenerator.java index 32ed9bb2a6..8747add450 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sYamlGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/K8sYamlGenerator.java @@ -59,7 +59,7 @@ protected void write(Model m) throws IOException { private void assemble(Writer w, MainInfo main) throws IOException { - for (ElementInfo element : getTopLevelElementInfos(main)) { + for (ElementInfo element : getComponentElementInfos(main)) { writeCRD(w, element); appendLine(w, "---"); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/SchemaObject.java b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/SchemaObject.java index b4b7ab035c..ef48959290 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/SchemaObject.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/SchemaObject.java @@ -28,11 +28,36 @@ public class SchemaObject extends AbstractSchema { private List> oneOf; + private List> allOf; + private final Map> patternProperties = new LinkedHashMap<>(); + SchemaObject(String name) { super(name); type = OBJECT; } + /** + * Populates the given {@code ObjectNode} with the JSON schema representation + * of this {@code SchemaObject}, including additional properties, pattern properties, + * and combinations (allOf, oneOf). + * + * @param node the {@code ObjectNode} to populate with schema details + * @return the modified {@code ObjectNode} containing the schema representation + */ + public ObjectNode json(ObjectNode node) { + super.json(node); + + if (!additionalProperties && isObject()) { + node.put("additionalProperties", false); + } + + addProperties(node); + addPatternProperties(node); + addAllOf(node); + addOneOf(node); + return node; + } + public SchemaObject property(AbstractSchema as) { for (AbstractSchema p : properties) if (p.getName().equals(as.getName())) @@ -46,27 +71,54 @@ public SchemaObject additionalProperties(boolean additionalProperties) { return this; } - public ObjectNode json(ObjectNode node) { - super.json(node); + public SchemaObject patternProperty(String pattern, AbstractSchema schema) { + patternProperties.put(pattern, schema); + return this; + } - if (!additionalProperties && isObject()) { - node.put("additionalProperties", false); + private void addOneOf(ObjectNode node) { + if (oneOf == null || oneOf.isEmpty()) + return; + + var oneOfArray = jnf.arrayNode(); + for (AbstractSchema s : oneOf) { + oneOfArray.add(s.json(jnf.objectNode())); } + node.set("oneOf", oneOfArray); + + } - jsonProperties(node); + private void addAllOf(ObjectNode node) { + if (allOf == null || allOf.isEmpty()) + return; - if (oneOf != null && !oneOf.isEmpty()) { - var oneOfArray = jnf.arrayNode(); - for (AbstractSchema s : oneOf) { - oneOfArray.add(s.json(jnf.objectNode())); - } - node.set("oneOf", oneOfArray); + var allOfArray = jnf.arrayNode(); + for (AbstractSchema s : allOf) { + allOfArray.add(s.json(jnf.objectNode())); } + node.set("allOf", allOfArray); - return node; } - private void jsonProperties(ObjectNode node) { + private void addPatternProperties(ObjectNode node) { + if (patternProperties.isEmpty()) + return; + + ObjectNode pp = jnf.objectNode(); + for (var e : patternProperties.entrySet()) { + pp.set(e.getKey(), e.getValue().json(jnf.objectNode())); + } + node.set("patternProperties", pp); + } + + /** + * Populates the specified {@code ObjectNode} with the properties of the JSON schema + * associated with this object. The method iterates over the defined properties, adding + * them to a "properties" node, and specifies any required properties in a "required" array. + * + * @param node the {@code ObjectNode} to populate with the properties and required fields + */ + private void addProperties(ObjectNode node) { if (properties.isEmpty()) return; @@ -87,7 +139,7 @@ private void jsonProperties(ObjectNode node) { node.set("properties", propertiesNode); } - private static ObjectNode createPropertyNode(AbstractSchemaproperty) { + private static ObjectNode createPropertyNode(AbstractSchema property) { ObjectNode propertyNode = property.json(jnf.objectNode()); if (property.getEnumValues() != null && !property.getEnumValues().isEmpty()) { propertyNode.set("enum", getEnumNode(property)); @@ -110,4 +162,13 @@ public SchemaObject oneOf(List> oneOf) { this.oneOf = oneOf; return this; } + + public SchemaObject allOf(List> allOf) { + this.allOf = allOf; + return this; + } + + public boolean hasProperty(String name) { + return properties.stream().anyMatch(p -> name.equals(p.getName())); + } } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/model/ElementInfo.java b/annot/src/main/java/com/predic8/membrane/annot/model/ElementInfo.java index 039e70d2da..305a7c4113 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/model/ElementInfo.java +++ b/annot/src/main/java/com/predic8/membrane/annot/model/ElementInfo.java @@ -13,125 +13,133 @@ limitations under the License. */ package com.predic8.membrane.annot.model; -import java.util.ArrayList; -import java.util.List; +import com.predic8.membrane.annot.*; -import javax.lang.model.element.TypeElement; +import javax.lang.model.element.*; +import java.util.*; -import com.predic8.membrane.annot.AnnotUtils; -import com.predic8.membrane.annot.MCElement; +import static com.predic8.membrane.annot.model.OtherAttributesInfo.ValueType.*; /** * Mirrors {@link MCElement}. */ public class ElementInfo extends AbstractJavadocedInfo { - private MCElement annotation; - private final List usedBy = new ArrayList<>(); - - - private TypeElement element; - private boolean hasIdField; - - private TextContentInfo tci; - - private List ais = new ArrayList<>(); - private List ceis = new ArrayList<>(); - - private OtherAttributesInfo oai; - - public TypeElement getElement() { - return element; - } - public void setElement(TypeElement element) { - this.element = element; - setDocedE(element); - } - public TextContentInfo getTci() { - return tci; - } - public void setTci(TextContentInfo tci) { - this.tci = tci; - } - public List getAis() { - return ais; - } - public void setAis(List ais) { - this.ais = ais; - } - public boolean isHasIdField() { - return hasIdField; - } - public void setHasIdField(boolean hasIdField) { - this.hasIdField = hasIdField; - } - public List getChildElementSpecs() { - return ceis; - } - public void setCeis(List ceis) { - this.ceis = ceis; - } - - public String getParserClassSimpleName() { - return AnnotUtils.javaify(getId().replace("-", "") + "Parser"); - } - - public MainInfo getMain(Model m) { - for (MainInfo main : m.getMains()) { - if (main.getAnnotation().outputPackage().equals(getAnnotation().configPackage())) - return main; - } - return m.getMains().getFirst(); - } - - public String getClassName(Model m) { - return getMain(m).getAnnotation().outputPackage() + "." + getParserClassSimpleName(); - } - - public String getXSDTypeName(Model m) { + private MCElement annotation; + private final List usedBy = new ArrayList<>(); + + + private TypeElement element; + private boolean hasIdField; + + private TextContentInfo tci; + + private final List ais = new ArrayList<>(); + private final List ceis = new ArrayList<>(); + + private OtherAttributesInfo oai; + + public TypeElement getElement() { + return element; + } + + public void setElement(TypeElement element) { + this.element = element; + setDocedE(element); + } + + public TextContentInfo getTci() { + return tci; + } + + public void setTci(TextContentInfo tci) { + this.tci = tci; + } + + public List getAis() { + return ais; + } + + public boolean isHasIdField() { + return hasIdField; + } + + public void setHasIdField(boolean hasIdField) { + this.hasIdField = hasIdField; + } + + public List getChildElementSpecs() { + return ceis; + } + + public String getParserClassSimpleName() { + return AnnotUtils.javaify(getId().replace("-", "") + "Parser"); + } + + public MainInfo getMain(Model m) { + for (MainInfo main : m.getMains()) { + if (main.getAnnotation().outputPackage().equals(getAnnotation().configPackage())) + return main; + } + return m.getMains().getFirst(); + } + + public String getClassName(Model m) { + return getMain(m).getAnnotation().outputPackage() + "." + getParserClassSimpleName(); + } + + public String getXSDTypeName(Model m) { // There are JSON Schema parsers that do not accept names like a.b.c - return getClassName(m).replace(".","_"); - } - - public MCElement getAnnotation() { - return annotation; - } - - public void setAnnotation(MCElement annotation) { - this.annotation = annotation; - } - - public void addUsedBy(ChildElementDeclarationInfo cedi) { - usedBy.add(cedi); - } - - public List getUsedBy() { - return usedBy; - } - - public String getId() { - if (!annotation.id().isEmpty()) - return annotation.id(); - return annotation.name(); - } - - public void setOai(OtherAttributesInfo oai) { - this.oai = oai; - } - - public OtherAttributesInfo getOai() { - return oai; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ElementInfo other)) - return false; + return getClassName(m).replace(".", "_"); + } + + public MCElement getAnnotation() { + return annotation; + } + + public void setAnnotation(MCElement annotation) { + this.annotation = annotation; + } + + public void addUsedBy(ChildElementDeclarationInfo cedi) { + usedBy.add(cedi); + } + + public List getUsedBy() { + return usedBy; + } + + public String getId() { + if (!annotation.id().isEmpty()) + return annotation.id(); + return annotation.name(); + } + + public void setOai(OtherAttributesInfo oai) { + this.oai = oai; + } + + public OtherAttributesInfo getOai() { + return oai; + } + + public boolean isString() { + return oai != null && oai.getValueType() == STRING; + } + + public boolean isObject() { + return oai != null && oai.getValueType() == OBJECT; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ElementInfo other)) + return false; return element.equals(other.element); - } + } - @Override - public int hashCode() { - return element.getQualifiedName().toString().hashCode(); - } + @Override + public int hashCode() { + return element.getQualifiedName().toString().hashCode(); + } } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/model/MainInfo.java b/annot/src/main/java/com/predic8/membrane/annot/model/MainInfo.java index 3deca1000f..9e9c9d526e 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/model/MainInfo.java +++ b/annot/src/main/java/com/predic8/membrane/annot/model/MainInfo.java @@ -67,7 +67,7 @@ public Map getElements() { return elements; } - public Map getTopLevels() { + public Map getComponents() { return globals; } diff --git a/annot/src/main/java/com/predic8/membrane/annot/model/OtherAttributesInfo.java b/annot/src/main/java/com/predic8/membrane/annot/model/OtherAttributesInfo.java index cb536e09c6..06a28a2a1f 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/model/OtherAttributesInfo.java +++ b/annot/src/main/java/com/predic8/membrane/annot/model/OtherAttributesInfo.java @@ -14,6 +14,9 @@ package com.predic8.membrane.annot.model; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; import com.predic8.membrane.annot.AnnotUtils; import com.predic8.membrane.annot.ProcessingException; @@ -21,6 +24,7 @@ public class OtherAttributesInfo extends AbstractJavadocedInfo { private ExecutableElement otherAttributesSetter; + private TypeElement mapValueType; public ExecutableElement getOtherAttributesSetter() { return otherAttributesSetter; @@ -29,6 +33,13 @@ public ExecutableElement getOtherAttributesSetter() { public void setOtherAttributesSetter(ExecutableElement otherAttributesSetter) { this.otherAttributesSetter = otherAttributesSetter; setDocedE(otherAttributesSetter); + + var typeArgs = ((DeclaredType) otherAttributesSetter.getParameters().getFirst().asType()).getTypeArguments(); + if (typeArgs.size() != 2) { + throw new ProcessingException("@MCOtherAttributes must use Map.", otherAttributesSetter); + } + + mapValueType = (TypeElement) ((DeclaredType) typeArgs.get(1)).asElement(); } public String getSpringName() { @@ -39,4 +50,18 @@ public String getSpringName() { return AnnotUtils.dejavaify(s); } + public ValueType getValueType() { + if (mapValueType.getQualifiedName().toString().equals("java.lang.String")) { + return ValueType.STRING; + } + if (mapValueType.getQualifiedName().toString().equals("java.lang.Object")) { + return ValueType.OBJECT; + } + throw new ProcessingException("Not supported: @McOtherAttributes void setAttr(Map attrs) where T is neither String nor Object."); + } + + public enum ValueType { + STRING, + OBJECT + } } diff --git a/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java b/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java new file mode 100644 index 0000000000..c55848d6aa --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java @@ -0,0 +1,54 @@ +package com.predic8.membrane.annot.util; + +public class ReflectionUtil { + + /** + * Converts a string literal to the target Java type. + */ + public static Object convert(String raw, Class targetType) { + if (targetType == String.class) return raw; + if (raw == null) { + if (targetType.isPrimitive()) + throw new IllegalArgumentException("Cannot assign null to primitive " + targetType.getName()); + return null; + } + + if (targetType == boolean.class || targetType == Boolean.class) return Boolean.parseBoolean(raw); + if (targetType == int.class || targetType == Integer.class) return Integer.parseInt(raw); + if (targetType == long.class || targetType == Long.class) return Long.parseLong(raw); + if (targetType == double.class || targetType == Double.class) return Double.parseDouble(raw); + if (targetType == float.class || targetType == Float.class) return Float.parseFloat(raw); + if (targetType == short.class || targetType == Short.class) return Short.parseShort(raw); + if (targetType == byte.class || targetType == Byte.class) return Byte.parseByte(raw); + if (targetType == char.class || targetType == Character.class) { + if (raw.length() != 1) throw new IllegalArgumentException("Expected single character, got: " + raw); + return raw.charAt(0); + } + + if (targetType.isEnum()) { + //noinspection unchecked + return Enum.valueOf((Class) targetType, raw); + } + + throw new IllegalArgumentException("Unsupported conversion to " + targetType.getName() + " from value: " + raw); + } + + /** + * Determines if the given wrapper class is the corresponding wrapper type + * for the specified primitive type. + * + * @param primitive the primitive type to be checked (e.g., int.class, double.class) + * @param wrapper the wrapper class to be checked (e.g., Integer.class, Double.class) + * @return true if the wrapper class is the corresponding wrapper for the primitive type, false otherwise + */ + public static boolean isWrapperOfPrimitive(Class primitive, Class wrapper) { + return (primitive == int.class && wrapper == Integer.class) + || (primitive == long.class && wrapper == Long.class) + || (primitive == boolean.class && wrapper == Boolean.class) + || (primitive == double.class && wrapper == Double.class) + || (primitive == float.class && wrapper == Float.class) + || (primitive == short.class && wrapper == Short.class) + || (primitive == byte.class && wrapper == Byte.class) + || (primitive == char.class && wrapper == Character.class); + } +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanDefinition.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanDefinition.java index 0c14290381..f1df1feae3 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanDefinition.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanDefinition.java @@ -14,9 +14,13 @@ package com.predic8.membrane.annot.yaml; import com.fasterxml.jackson.databind.JsonNode; +import com.predic8.membrane.annot.*; +import com.predic8.membrane.annot.bean.*; import java.util.*; +import static com.predic8.membrane.annot.yaml.WatchAction.*; + public class BeanDefinition { public static final String PROTOTYPE = "prototype"; @@ -27,6 +31,10 @@ public class BeanDefinition { private final JsonNode node; private final WatchAction action; private final String kind; + + /** + * Constructed bean after initialization. + */ private Object bean; /** @@ -57,7 +65,7 @@ public BeanDefinition(String kind, String name, String namespace, String uid, Js this.namespace = namespace; this.uid = uid; this.node = node; - this.action = WatchAction.ADDED; + this.action = ADDED; } public JsonNode getNode() { @@ -100,10 +108,31 @@ public String getScope() { JsonNode annotations = meta.get("annotations"); if (annotations == null) return null; - return annotations.get("membrane-soa.org/scope").asText(); // TODO migrate to membrane-api.io + JsonNode scope = annotations.get("membrane-api.io/scope"); + return scope == null ? null : scope.asText(); + } + + public boolean isComponent() { + return name != null && name.startsWith("#/components/"); + } + + public boolean isBean() { + return "bean".equals(kind); } public boolean isPrototype() { return PROTOTYPE.equals(getScope()); } + + public boolean isDeleted() { + return action == DELETED; + } + + public boolean isModified() { + return action == MODIFIED; + } + + public boolean isAdded() { + return action == ADDED; + } } diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanRegistryImplementation.java index 7ed672c7e0..7a3edddade 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanRegistryImplementation.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/BeanRegistryImplementation.java @@ -13,16 +13,19 @@ limitations under the License. */ package com.predic8.membrane.annot.yaml; -import com.fasterxml.jackson.databind.*; -import com.predic8.membrane.annot.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.predic8.membrane.annot.Grammar; +import com.predic8.membrane.annot.bean.BeanFactory; import org.jetbrains.annotations.*; -import org.slf4j.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.*; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; -import static com.predic8.membrane.annot.yaml.BeanDefinition.*; +import static com.predic8.membrane.annot.yaml.BeanDefinition.create4Kubernetes; import static com.predic8.membrane.annot.yaml.WatchAction.*; public class BeanRegistryImplementation implements BeanRegistry { @@ -71,12 +74,19 @@ public void start() { } } - private Object define(BeanDefinition bd) throws IOException, ParsingException { + private Object define(BeanDefinition bd) { log.debug("defining bean: {}", bd.getNode()); - return GenericYamlParser.readMembraneObject(bd.getKind(), - grammar, - bd.getNode(), - this); + try { + if ("bean".equals(bd.getKind())) { + return new BeanFactory(this).create(bd.getNode().path("bean")); + } + return GenericYamlParser.readMembraneObject(bd.getKind(), + grammar, + bd.getNode(), + this); + } catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -107,8 +117,9 @@ void handle(BeanDefinition bd) { // can see both metadata and the action (including DELETED). bds.put(bd.getUid(), bd); - if (observer.isActivatable(bd)) + if (!bd.isComponent() && observer.isActivatable(bd)) { uidsToActivate.add(bd.getUid()); + } if (changeEvents.isEmpty()) activationRun(); @@ -122,16 +133,12 @@ private void activationRun() { Object bean = define(bd); bd.setBean(bean); - Object oldBean = null; - if (bd.getAction() == MODIFIED || bd.getAction() == DELETED) - oldBean = uuidMap.get(bd.getUid()); - // e.g. inform router about new proxy - observer.handleBeanEvent(bd, bean, oldBean); + observer.handleBeanEvent(bd, bean, getOldBean(bd)); - if (bd.getAction() == ADDED || bd.getAction() == MODIFIED) + if (bd.isAdded() || bd.isModified()) uuidMap.put(bd.getUid(), bean); - if (bd.getAction() == DELETED) { + if (bd.isDeleted()) { uuidMap.remove(bd.getUid()); bds.remove(bd.getUid()); } @@ -145,39 +152,53 @@ private void activationRun() { uidsToActivate.remove(uid); } + private @Nullable Object getOldBean(BeanDefinition bd) { + Object oldBean = null; + if (bd.isModified() || bd.isDeleted()) + oldBean = uuidMap.get(bd.getUid()); + return oldBean; + } + @Override public Object resolveReference(String url) { BeanDefinition bd = getFirstByName(url).orElseThrow(() -> new RuntimeException("Reference %s not found".formatted(url))); - Object envelope = null; - if (bd.getBean() != null) - envelope = bd.getBean(); - if (envelope == null) { - try { - envelope = define(bd); - } catch (IOException e) { - throw new RuntimeException(e); - } - if (!bd.isPrototype()) - bd.setBean(envelope); - } - return envelope; - // TODO -// if (spec instanceof Bean) -// return ((Bean) spec).getBean(); + boolean prototype = isPrototypeScope(bd); + + if (!prototype && bd.getBean() != null) + return bd.getBean(); + + Object instance = define(bd); + + if (!prototype) + bd.setBean(instance); + + return instance; } private @NotNull Optional getFirstByName(String url) { - return bds.values().stream().filter(bd -> bd.getName().equals(url)).findFirst(); + return bds.values().stream().filter(bd -> url.equals(bd.getName())).findFirst(); } @Override public List getBeans() { - return bds.values().stream().map(BeanDefinition::getBean).filter(Objects::nonNull).toList(); + return bds.values().stream().filter(bd -> !bd.isComponent()) + .map(BeanDefinition::getBean) + .filter(Objects::nonNull) + .toList(); } @Override public Grammar getGrammar() { return grammar; } + + private static boolean isPrototypeScope(BeanDefinition bd) { + if (!bd.isBean()) + return bd.isPrototype(); + + return "PROTOTYPE".equalsIgnoreCase( + bd.getNode().path("bean").path("scope").asText("SINGLETON") + ); + } } diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java index bfcf530d18..a02006c744 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java @@ -15,6 +15,8 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.*; import com.networknt.schema.Error; import com.predic8.membrane.annot.*; @@ -24,7 +26,6 @@ import java.io.*; import java.lang.reflect.*; import java.util.*; -import java.util.stream.Collectors; import static com.networknt.schema.SpecificationVersion.*; import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*; @@ -53,10 +54,9 @@ public class GenericYamlParser { */ public GenericYamlParser(Grammar grammar, String yaml) throws IOException { JsonLocationMap jsonLocationMap = new JsonLocationMap(); - List rootNodes = jsonLocationMap.parseWithLocations(yaml); var idx = 0; - for (JsonNode jsonNode : rootNodes) { + for (JsonNode jsonNode : jsonLocationMap.parseWithLocations(yaml)) { if (jsonNode == null) { log.debug(GenericYamlParser.EMPTY_DOCUMENT_WARNING); continue; @@ -74,6 +74,10 @@ public GenericYamlParser(Grammar grammar, String yaml) throws IOException { location.getColumnNr()), e); } + if ("components".equals(getBeanType(jsonNode))) { + beanDefs.addAll(extractComponentBeanDefinitions(jsonNode.get("components"))); + } + beanDefs.add(new BeanDefinition( getBeanType(jsonNode), "bean-" + idx++, @@ -159,29 +163,30 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js T configObj = clazz.getConstructor().newInstance(); if (node.isArray()) { // when this is a list, we are on a @MCElement(..., noEnvelope=true) - Method method = getSingleChildSetter(clazz); - method.invoke(configObj, (Object) parseListExcludingStartEvent(ctx, node)); + method.invoke(configObj, parseListExcludingStartEvent(ctx, node)); return configObj; } ensureMappingStart(node); if (isNoEnvelope(clazz)) throw new RuntimeException("Class " + clazz.getName() + " is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list."); + JsonNode refNode = node.get("$ref"); + if (refNode != null) { + applyObjectLevelRef(ctx, clazz, node, refNode, configObj); + } + List required = findRequiredSetters(clazz); for (Iterator it = node.fieldNames(); it.hasNext(); ) { String key = it.next(); - try { - - if ("$ref".equals(key)) { - handleTopLevelRefs(clazz, node.get(key), ctx.registry(), configObj); - continue; - } + if ("$ref".equals(key)) + continue; + try { MethodSetter methodSetter = getMethodSetter(ctx, clazz, key); required.remove(methodSetter.getSetter()); - methodSetter.setSetter(configObj,ctx,node,key); + methodSetter.setSetter(configObj, ctx, node, key); } catch (Throwable cause) { throw new ParsingException(cause, node.get(key)); } @@ -194,10 +199,72 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js } } - private static void handleTopLevelRefs(Class clazz, JsonNode node, BeanRegistry registry, T obj) throws InvocationTargetException, IllegalAccessException { - ensureTextual(node, "Expected a string after the '$ref' key."); - Object o = registry.resolveReference(node.asText()); - getChildSetter(clazz, o.getClass()).invoke(obj, o); + private static List extractComponentBeanDefinitions(JsonNode componentsNode) { + if (componentsNode == null || componentsNode.isNull()) + return List.of(); + + if (!componentsNode.isObject()) + throw new ParsingException("Expected object for 'components'.", componentsNode); + + List res = new ArrayList<>(); + + Iterator ids = componentsNode.fieldNames(); + while (ids.hasNext()) { + String id = ids.next(); + JsonNode def = componentsNode.get(id); + + // Each component definition must have exactly one key (the component type) + ensureSingleKey(def); + String componentKind = def.fieldNames().next(); + + // Wrap it into a normal top-level node: { : } + ObjectNode wrapped = JsonNodeFactory.instance.objectNode(); + wrapped.set(componentKind, def.get(componentKind)); + + res.add(new BeanDefinition( + componentKind, + "#/components/" + id, + "default", + randomUUID().toString(), + wrapped + )); + } + return res; + } + + /** + * Applies an object-level "$ref" by resolving the referenced component and injecting it + * into the parent object via the matching @MCChildElement setter. + * Rejects "$ref" if the same child is already configured inline. + */ + private static void applyObjectLevelRef(ParsingContext ctx, Class parentClass, JsonNode parentNode, JsonNode refNode, T obj) throws ParsingException { + ensureTextual(refNode, "Expected a string after the '$ref' key."); + Object referenced = getReferenced(ctx, refNode); + String refKey = getElementName(referenced.getClass()); + + // Forbid inline + $ref for the same child + if (parentNode.has(refKey)) { + throw new ParsingException("Cannot use '$ref' together with inline '%s' in '%s'." + .formatted(refKey, ctx.context()), parentNode.get(refKey)); + } + + try { + getChildSetter(parentClass, referenced.getClass()).invoke(obj, referenced); + } catch (RuntimeException e) { + throw new ParsingException( + "Referenced component '%s' (type '%s') is not allowed in '%s'." + .formatted(refNode.asText(), refKey, ctx.context()), refNode); + } catch (Throwable t) { + throw new ParsingException(t, refNode); + } + } + + private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { + try { + return ctx.registry().resolveReference(refNode.asText()); + } catch (RuntimeException e) { + throw new ParsingException(e, refNode); + } } public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { @@ -205,8 +272,8 @@ public static List parseListIncludingStartEvent(ParsingContext context, return parseListExcludingStartEvent(context, node); } - private static @NotNull ArrayList parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { - ArrayList res = new ArrayList<>(); + private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + List res = new ArrayList<>(); for (int i = 0; i < node.size(); i++) { res.add(parseMapToObj(context, node.get(i))); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/McYamlIntrospector.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/McYamlIntrospector.java index 229505dbce..d392b4cf2b 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/McYamlIntrospector.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/McYamlIntrospector.java @@ -15,14 +15,15 @@ package com.predic8.membrane.annot.yaml; import com.predic8.membrane.annot.*; +import org.jetbrains.annotations.*; -import java.lang.reflect.Method; -import java.util.List; -import java.util.stream.Collectors; +import java.lang.reflect.*; +import java.util.*; +import java.util.stream.*; -import static java.lang.Character.toLowerCase; -import static java.util.Arrays.stream; -import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; +import static java.lang.Character.*; +import static java.util.Arrays.*; +import static org.springframework.core.annotation.AnnotationUtils.*; public final class McYamlIntrospector { @@ -41,25 +42,35 @@ public static boolean isStructured(Method method) { public static boolean matchesJsonKey(Method method, String key) { return matchesJsonChildElementKey(method, key) - || equalsTextContent(method, key) - || equalsAttributeName(method, key); + || equalsTextContent(method, key) + || equalsAttributeName(method, key); } private static boolean matchesJsonChildElementKey(Method method, String key) { return findAnnotation(method, MCChildElement.class) != null - && method.getName().substring(3).equalsIgnoreCase(key); + && matchesPropertyName(method, key); } private static boolean equalsTextContent(Method method, String key) { - return findAnnotation(method, MCTextContent.class) != null && method.getName().substring(3).equalsIgnoreCase(key); + return findAnnotation(method, MCTextContent.class) != null && matchesPropertyName(method, key); } private static boolean equalsAttributeName(Method method, String key) { MCAttribute annotation = findAnnotation(method, MCAttribute.class); if (annotation == null) return false; - return method.getName().substring(3).equalsIgnoreCase(key) && "".equals(annotation.attributeName()) - || annotation.attributeName().equals(key); + return matchesPropertyName(method, key) && "".equals(annotation.attributeName()) + || annotation.attributeName().equals(key); + } + + /** + * If key is "foo", then method name matches "setFoo", "getFoo". + * + * @param method Method to check. + * @param propertyName Property name to check. + */ + private static boolean matchesPropertyName(Method method, String propertyName) { + return method.getName().substring(3).equalsIgnoreCase(propertyName); } /** @@ -76,11 +87,17 @@ public static Method getSingleChildSetter(Class clazz) { if (annotation == null || !annotation.noEnvelope()) { throw new RuntimeException("Class " + clazz.getName() + " has properties, and is not a list."); } - if (stream(clazz.getMethods()) - .filter(McYamlIntrospector::isSetter) - .anyMatch(method -> findAnnotation(method, MCAttribute.class) != null)) { - throw new RuntimeException("Class " + clazz.getName() + " should not have any @MCAttribute setters, because it is a @MCElement with noEnvelope=true ."); + guardHasMCAttributeSetters(clazz); + Method setter = getChildSetters(clazz).getFirst(); + Class paramType = setter.getParameterTypes()[0]; + if (!java.util.Collection.class.isAssignableFrom(paramType)) { + throw new RuntimeException("The single @MCChildElement setter in " + clazz.getName() + + " must accept a Collection/List when noEnvelope=true, but found: " + paramType.getName()); } + return setter; + } + + private static @NotNull List getChildSetters(Class clazz) { List childSetters = stream(clazz.getMethods()) .filter(McYamlIntrospector::isSetter) .filter(method -> findAnnotation(method, MCChildElement.class) != null) @@ -91,13 +108,15 @@ public static Method getSingleChildSetter(Class clazz) { if (childSetters.size() > 1) { throw new RuntimeException("Multiple @MCChildElement setters found in " + clazz.getName() + ". Only one is allowed when noEnvelope=true."); } - Method setter = childSetters.getFirst(); - Class paramType = setter.getParameterTypes()[0]; - if (!java.util.Collection.class.isAssignableFrom(paramType)) { - throw new RuntimeException("The single @MCChildElement setter in " + clazz.getName() + - " must accept a Collection/List when noEnvelope=true, but found: " + paramType.getName()); + return childSetters; + } + + private static void guardHasMCAttributeSetters(Class clazz) { + if (stream(clazz.getMethods()) + .filter(McYamlIntrospector::isSetter) + .anyMatch(method -> findAnnotation(method, MCAttribute.class) != null)) { + throw new RuntimeException("Class " + clazz.getName() + " should not have any @MCAttribute setters, because it is a @MCElement with noEnvelope=true ."); } - return setter; } public static Method findSetterForKey(Class clazz, String key) { @@ -111,7 +130,7 @@ public static Method findSetterForKey(Class clazz, String key) { public static List findRequiredSetters(Class clazz) { return stream(clazz.getMethods()) .filter(McYamlIntrospector::isSetter) - .filter(method -> isRequired(method)) + .filter(McYamlIntrospector::isRequired) .collect(Collectors.toList()); } @@ -141,11 +160,9 @@ public static Method getChildSetter(Class clazz, Class valueClass) { .filter(method -> method.getParameterTypes().length == 1) .filter(method -> method.getParameterTypes()[0].isAssignableFrom(valueClass)) .reduce((a, b) -> { - throw new RuntimeException("Multiple potential setters found on " - + clazz.getName() + " for value of type " + valueClass.getName()); + throw new RuntimeException("Multiple potential setters found on %s for value of type %s".formatted(clazz.getName(), valueClass.getName())); }) - .orElseThrow(() -> new RuntimeException("Could not find child setter on " - + clazz.getName() + " for value of type " + valueClass.getName())); + .orElseThrow(() -> new RuntimeException("Could not find child setter on %s for value of type %s".formatted(clazz.getName(), valueClass.getName()))); } public static boolean isReferenceAttribute(Method setter) { @@ -158,4 +175,11 @@ public static boolean hasOtherAttributes(Method setter) { return findAnnotation(setter, MCOtherAttributes.class) != null; } + public static String getElementName(Class type) { + MCElement ann = type.getAnnotation(MCElement.class); + if (ann != null && ann.name() != null && !ann.name().isBlank()) + return ann.name(); + return type.getSimpleName(); + } + } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java index 768c2038c1..a1f90df696 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java @@ -14,17 +14,19 @@ package com.predic8.membrane.annot.yaml; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.JsonNode; import com.predic8.membrane.annot.MCChildElement; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.*; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; +import javax.lang.model.util.Types; +import java.lang.reflect.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; -import static com.predic8.membrane.annot.yaml.GenericYamlParser.*; +import static com.predic8.membrane.annot.yaml.GenericYamlParser.createAndPopulateNode; +import static com.predic8.membrane.annot.yaml.GenericYamlParser.parseListIncludingStartEvent; import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*; -import static com.predic8.membrane.annot.yaml.McYamlIntrospector.findSetterForKey; import static java.lang.Boolean.parseBoolean; import static java.lang.Integer.parseInt; import static java.lang.Long.parseLong; @@ -82,8 +84,9 @@ public void setSetter(T instance, ParsingContext ctx, JsonNode node, String private Object resolveSetterValue(ParsingContext ctx, JsonNode node, String key) throws WrongEnumConstantException, ParsingException { Class wanted = getParameterType(); - if (Collection.class.isAssignableFrom(wanted)) - return parseListIncludingStartEvent(ctx, node); + + List list = getObjectList(ctx, node, key, wanted); + if (list != null) return list; if (wanted.isEnum()) return parseEnum(wanted, node); if (wanted.equals(String.class)) return node.asText(); @@ -91,8 +94,12 @@ private Object resolveSetterValue(ParsingContext ctx, JsonNode node, String key) if (wanted == Integer.TYPE || wanted == Integer.class) return parseInt(node.asText()); if (wanted == Long.TYPE || wanted == Long.class) return parseLong(node.asText()); if (wanted == Boolean.TYPE || wanted == Boolean.class) return parseBoolean(node.asText()); - if (wanted.equals(Map.class) && McYamlIntrospector.hasOtherAttributes(setter)) return Map.of(key, node.asText()); + + if (node.isTextual() && isBeanReference(wanted)) { + return resolveReference(ctx, node, key, wanted); + } + if (McYamlIntrospector.isStructured(setter)) { if (beanClass != null) return createAndPopulateNode(ctx.updateContext(key), beanClass, node); return createAndPopulateNode(ctx.updateContext(key), wanted, node); @@ -101,6 +108,52 @@ private Object resolveSetterValue(ParsingContext ctx, JsonNode node, String key) throw new RuntimeException("Not implemented setter type " + wanted); } + private @Nullable List getObjectList(ParsingContext ctx, JsonNode node, String key, Class wanted) { + if (Collection.class.isAssignableFrom(wanted)) { + List list = parseListIncludingStartEvent(ctx, node); + + Class elemType = getCollectionElementType(setter); + if (elemType != null) { + for (Object o : list) { + if (o == null) continue; + if (!elemType.isAssignableFrom(o.getClass())) { + throw new ParsingException("Value of type '%s' is not allowed in list '%s'. Expected '%s'." + .formatted(McYamlIntrospector.getElementName(o.getClass()), key, elemType.getSimpleName()), node); + } + } + } + return list; + } + return null; + } + + private static @NotNull Object resolveReference(ParsingContext ctx, JsonNode node, String key, Class wanted) { + String ref = node.asText(); + final Object resolved; + try { + resolved = ctx.registry().resolveReference(ref); + } catch (RuntimeException e) { + throw new ParsingException(e, node); + } + if (!wanted.isAssignableFrom(resolved.getClass())) { + throw new ParsingException( + "Referenced bean '%s' has type '%s' but '%s' expects '%s'." + .formatted(ref, resolved.getClass().getName(), key, wanted.getName()), + node + ); + } + return resolved; + } + + /** + * Mirrors {@link com.predic8.membrane.annot.model.AttributeInfo#analyze(Types)}. + */ + private boolean isBeanReference(Class wanted) { + if (wanted == Integer.TYPE || wanted == Long.TYPE || wanted == Float.TYPE || wanted == Double.TYPE || wanted == Boolean.TYPE || wanted == String.class) + return false; + return !wanted.isEnum(); + } + public Method getSetter() { return setter; } @@ -111,12 +164,23 @@ public Class getBeanClass() { private static > E parseEnum(Class enumClass, JsonNode node) throws WrongEnumConstantException { String value = node.asText().toUpperCase(ROOT); - @SuppressWarnings("unchecked") - Class castEnumClass = (Class) enumClass; try { - return Enum.valueOf(castEnumClass, value); + return Enum.valueOf((Class) enumClass, value); } catch (IllegalArgumentException e) { throw new WrongEnumConstantException(enumClass, value); } } + + private static Class getCollectionElementType(Method setter) { + Type t = setter.getGenericParameterTypes()[0]; + if (!(t instanceof ParameterizedType pt)) return null; + Type arg = pt.getActualTypeArguments()[0]; + if (arg instanceof Class c) return c; + if (arg instanceof WildcardType wt) { + Type[] upper = wt.getUpperBounds(); + if (upper.length == 1 && upper[0] instanceof Class uc) return uc; + } + if (arg instanceof ParameterizedType p2 && p2.getRawType() instanceof Class rc) return rc; + return null; + } } diff --git a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java index 4b9aeedb32..77325e596b 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java @@ -48,7 +48,7 @@ public class DemoElement { var result = CompilerHelper.compile(sources, false); assertCompilerResult(true, result); - parse(result, wrapSpring(""" + parseXML(result, wrapSpring(""" """)); } @@ -84,7 +84,7 @@ public class Child2 extends AbstractDemoChildElement { var result = CompilerHelper.compile(sources, false); assertCompilerResult(true, result); - parse(result, wrapSpring(""" + parseXML(result, wrapSpring(""" diff --git a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java index 6ab796ebd8..e2f0d3d413 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/SpringConfigXSDErrorsTest.java @@ -113,7 +113,7 @@ public class DemoElement2 { var result = CompilerHelper.compile(sources, false); assertCompilerResult(false, of( - error("Duplicate top-level @MCElement name. Make at least one @MCElement(topLevel=false,...) ."), + error("Duplicate component @MCElement name. Make at least one @MCElement(component=false,...) ."), error("also here") ), result); } @@ -121,28 +121,12 @@ public class DemoElement2 { @Nested class NoEnvelope { - @Test - public void topLevel() { - var sources = splitSources(MC_MAIN_DEMO + """ - package com.predic8.membrane.demo; - import com.predic8.membrane.annot.MCElement; - @MCElement(name="demo", noEnvelope=true) - public class DemoElement { - } - """); - var result = CompilerHelper.compile(sources, false); - - assertCompilerResult(false, of( - error("@MCElement(..., noEnvelope=true, topLevel=true) is invalid.") - ), result); - } - @Test public void mixed() { var sources = splitSources(MC_MAIN_DEMO + """ package com.predic8.membrane.demo; import com.predic8.membrane.annot.MCElement; - @MCElement(name="demo", noEnvelope=true, topLevel=false, mixed=true) + @MCElement(name="demo", noEnvelope=true, component=false, mixed=true) public class DemoElement { } """); @@ -158,7 +142,7 @@ public void noChildElements() { var sources = splitSources(MC_MAIN_DEMO + """ package com.predic8.membrane.demo; import com.predic8.membrane.annot.MCElement; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { } """); @@ -176,7 +160,7 @@ public void twoChildElements() { import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.annot.MCChildElement; import java.util.List; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { @MCChildElement(order=1) public void setChild1(List s) {} @@ -197,7 +181,7 @@ public void childIsNotAList() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.annot.MCChildElement; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { @MCChildElement public void setChild1(DemoElement s) {} @@ -216,7 +200,7 @@ public void hasAttributes() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { @MCChildElement public void setChild1(List s) {} @@ -238,7 +222,7 @@ public void otherAttributes() { import com.predic8.membrane.annot.*; import java.util.List; import java.util.Map; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { @MCChildElement public void setChild1(List s) {} @@ -260,7 +244,7 @@ public void textContent() { import com.predic8.membrane.annot.*; import java.util.List; import java.util.Map; - @MCElement(name="demo", noEnvelope=true, topLevel=false) + @MCElement(name="demo", noEnvelope=true, component=false) public class DemoElement { @MCChildElement public void setChild1(List s) {} @@ -354,13 +338,13 @@ public abstract class AbstractDemoChildElement { --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child", topLevel=false, id="child1") + @MCElement(name="child", component=false, id="child1") public class Child1 extends AbstractDemoChildElement { } --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child", topLevel=false, id="child2") + @MCElement(name="child", component=false, id="child2") public class Child2 extends AbstractDemoChildElement { } """); diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java new file mode 100644 index 0000000000..4657d7101a --- /dev/null +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java @@ -0,0 +1,237 @@ +/* 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; + +import com.predic8.membrane.annot.util.CompilerHelper; +import com.predic8.membrane.annot.yaml.BeanRegistry; +import com.predic8.membrane.annot.yaml.YamlSchemaValidationException; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.List; + +import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO; +import static com.predic8.membrane.annot.util.CompilerHelper.*; +import static org.junit.jupiter.api.Assertions.*; + +public class YAMLBeanParsingTest { + + @Test + void beanComponentIsInstantiatedAndInjectedViaRefInList() { + BeanRegistry r = parse(""" + components: + dep: + bean: + class: com.predic8.membrane.demo.Dep + myBean: + bean: + class: com.predic8.membrane.demo.MyBean + scope: singleton + constructorArgs: + - constructorArg: { value: "8080" } + - constructorArg: { ref: "#/components/dep" } + properties: + - property: { name: "name", value: "abc" } + - property: { name: "l", value: "7" } + - property: { name: "d", value: "1.5" } + --- + holder: + items: + - $ref: "#/components/myBean" + """); + + Object holder = firstBeanBySimpleName(r, "HolderElement"); + @SuppressWarnings("unchecked") + List items = (List) call(holder, "getItems"); + + assertEquals(1, items.size()); + Object myBean = items.getFirst(); + + assertEquals("MyBean", myBean.getClass().getSimpleName()); + assertEquals(8080, ((Number) call(myBean, "getPort")).intValue()); + assertEquals("abc", call(myBean, "getName")); + assertEquals(7L, ((Number) call(myBean, "getL")).longValue()); + assertEquals(1.5d, ((Number) call(myBean, "getD")).doubleValue()); + assertNotNull(call(myBean, "getDep")); + assertEquals("Dep", call(myBean, "getDep").getClass().getSimpleName()); + } + + @Test + void singletonResolveReference() { + BeanRegistry r = parse(""" + components: + s: + bean: + class: com.predic8.membrane.demo.Counting + scope: singleton + """); + + Object s1 = r.resolveReference("#/components/s"); + Object s2 = r.resolveReference("#/components/s"); + assertSame(s1, s2, "singleton must return same instance"); + assertEquals(call(s1, "getId"), call(s2, "getId")); + } + + @Test + void prototypeResolveReference() { + BeanRegistry r = parse(""" + components: + p: + bean: + class: com.predic8.membrane.demo.Counting + scope: prototype + """); + + Object p1 = r.resolveReference("#/components/p"); + Object p2 = r.resolveReference("#/components/p"); + assertNotSame(p1, p2, "prototype must return new instance"); + assertNotEquals(call(p1, "getId"), call(p2, "getId")); + } + + @Test + void missingClassFailsFastOnResolve() { + BeanRegistry r = parse(""" + components: + x: + bean: + scope: singleton + """); + + var ex = assertThrows(RuntimeException.class, () -> r.resolveReference("#/components/x")); + assertAnyErrorContains(ex, "Missing/blank 'class'"); + } + + @Test + void unknownBeanPropertyIsSchemaError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + x: + bean: + class: com.predic8.membrane.demo.Dep + doesNotExist: 1 + """)); + assertSchemaErrorContains(ex, "doesNotExist", "is not defined in the schema and the schema does not allow additional properties"); + } + + + private BeanRegistry parse(String yaml) { + var sources = splitSources(MC_MAIN_DEMO + BEAN_DEMO_SOURCES); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + return parseYAML(result, yaml); + } + + private static Object firstBeanBySimpleName(BeanRegistry r, String simpleName) { + return r.getBeans().stream() + .filter(b -> b != null && b.getClass().getSimpleName().equals(simpleName)) + .findFirst() + .orElseThrow(() -> new AssertionError("Bean not found: " + simpleName)); + } + + private static Object call(Object target, String method) { + try { + Method m = target.getClass().getMethod(method); + m.setAccessible(true); + return m.invoke(target); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void assertSchemaErrorContains(RuntimeException ex, String... needles) { + var root = getRootCause(ex); + if (!(root instanceof YamlSchemaValidationException yse)) + throw new AssertionError("Expected YamlSchemaValidationException but got: " + root, root); + + assertFalse(yse.getErrors().isEmpty(), "Expected schema errors."); + + for (var n : needles) { + boolean found = yse.getErrors().stream().anyMatch(err -> { + String msg = err.getMessage(); + String s1 = msg != null ? msg : ""; + String s2 = err.toString(); + return s1.contains(n) || s2.contains(n); + }); + assertTrue(found, () -> "Expected schema error to contain '" + n + "' but was: " + yse.getErrors()); + } + } + + private void assertAnyErrorContains(RuntimeException ex, String... needles) { + var root = getRootCause(ex); + var msg = root.getMessage() != null ? root.getMessage() : root.toString(); + for (var n : needles) + assertTrue(msg.toLowerCase().contains(n.toLowerCase()), + () -> "Expected error to contain '" + n + "' but was: " + msg); + } + + private Throwable getRootCause(Throwable e) { + return (e.getCause() == null) ? e : getRootCause(e.getCause()); + } + + private static final String BEAN_DEMO_SOURCES = """ + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + + @MCElement(name="holder", topLevel=true) + public class HolderElement { + private List items; + + public List getItems() { return items; } + + @MCChildElement + public void setItems(List items) { this.items = items; } + } + --- + package com.predic8.membrane.demo; + public class Dep {} + --- + package com.predic8.membrane.demo; + + public class MyBean { + private final int port; + private final Dep dep; + private String name; + private long l; + public double d; + + public MyBean(int port, Dep dep) { + this.port = port; + this.dep = dep; + } + + public int getPort() { return port; } + public Dep getDep() { return dep; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public long getL() { return l; } + private void setL(long l) { this.l = l; } + + public double getD() { return d; } + } + --- + package com.predic8.membrane.demo; + + import java.util.concurrent.atomic.AtomicInteger; + + public class Counting { + private static final AtomicInteger C = new AtomicInteger(); + private final int id = C.incrementAndGet(); + public int getId() { return id; } + } + """; +} diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java new file mode 100644 index 0000000000..a4e2fe0f03 --- /dev/null +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java @@ -0,0 +1,605 @@ +/* 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; + +import com.predic8.membrane.annot.util.CompilerHelper; +import com.predic8.membrane.annot.yaml.BeanRegistry; +import com.predic8.membrane.annot.yaml.YamlSchemaValidationException; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO; +import static com.predic8.membrane.annot.util.CompilerHelper.*; +import static com.predic8.membrane.annot.util.StructureAssertionUtil.*; +import static org.junit.jupiter.api.Assertions.*; + +public class YAMLComponentsParsingTest { + + @Test + public void componentsEmpty() { + assertStructure( + parse(""" + components: {} + """), + clazz("Components") + ); + } + + @Test + public void componentsAndApiEmptySameDocument() { + assertStructure( + parseDocs(""" + components: {} + --- + api: {} + """), + clazz("Components"), + clazz("ApiElement") + ); + } + + + @Test + public void componentsSingleDefinition() { + assertStructure( + parse(""" + components: + sad: + demoBean: {} + """), + clazz("Components") + ); + } + + @Test + public void refAsFlowListItem() { + assertStructure( + parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/components/auth1" + """), + clazz("Components"), + clazz("ApiElement", + property("flow", list( + clazz("BasicAuthenticationElement", + property("fileUserDataProvider", + clazz("FileUserDataProviderElement", + property("htpasswdPath", value("/etc/htpasswd"))))))))); + } + + @Test + public void refAndInlineMixInFlow() { + assertStructure( + parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/components/auth1" + - template: + location: classpath:/t.xml + """), + clazz("Components"), + clazz("ApiElement", + property("flow", list( + clazz("BasicAuthenticationElement", + property("fileUserDataProvider", + clazz("FileUserDataProviderElement", + property("htpasswdPath", value("/etc/htpasswd"))))), + clazz("TemplateElement", + property("location", value("classpath:/t.xml"))) + ))) + ); + } + + @Test + public void sameRefUsedMultipleTimesInFlow() { + assertStructure( + parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/components/auth1" + - $ref: "#/components/auth1" + """), + clazz("Components"), + clazz("ApiElement", + property("flow", list( + clazz("BasicAuthenticationElement", + property("fileUserDataProvider", + clazz("FileUserDataProviderElement", + property("htpasswdPath", value("/etc/htpasswd"))))), + clazz("BasicAuthenticationElement", + property("fileUserDataProvider", + clazz("FileUserDataProviderElement", + property("htpasswdPath", value("/etc/htpasswd"))))) + ))) + ); + } + + @Test + public void refListItemWithExtraPropertyError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/components/auth1" + template: + location: classpath:/x.xml + """)); + assertAnyErrorContains(ex, "$ref"); + } + + @Test + public void refToUnknownComponentIdError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/components/doesNotExist" + """)); + assertAnyErrorContains(ex, "doesNotExist"); + } + + @Test + public void invalidRefPointerError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + auth1: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - $ref: "#/not-components/auth1" + """)); + assertAnyErrorContains(ex, "Reference #/not-components/auth1 not found"); + } + + @Test + public void componentDefinitionWithUnknownComponentKeyError() { + assertSchemaErrorContains(assertThrows(RuntimeException.class, () -> parse(""" + components: + x: + doesNotExist: {} + """)), "doesNotExist"); + } + + @Test + public void componentDefinitionWithNoComponentKeyError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + x: {} + """)); + assertSchemaErrorContains(ex, "required property", "not found"); + } + + @Test + public void componentDefinitionWithMultipleComponentKeysError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + x: + demoBean: {} + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + """)); + assertSchemaErrorContains(ex, "is not defined in the schema and the schema does not allow additional properties"); + } + + @Test + public void refInsideObjectLevel() { + assertStructure( + parse(""" + components: + manager: + bearerToken: + header: Authorization + --- + api: + flow: + - oauth2authserver: + issuer: https://issuer + otherFields: abc + $ref: "#/components/manager" + """), + clazz("Components"), + clazz("ApiElement", + property("flow", list( + clazz("OAuth2AuthServerElement", + property("issuer", value("https://issuer")), + property("otherFields", value("abc")), + property("bearerToken", + clazz("BearerTokenElement", + property("header", value("Authorization"))))) + ))) + ); + } + + @Test + public void objectLevelRefAndInlineForbidden() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + manager: + bearerToken: + header: Authorization + --- + api: + flow: + - oauth2authserver: + issuer: https://issuer + bearerToken: + header: Inline + $ref: "#/components/manager" + """)); + assertAnyErrorContains(ex, "Cannot use '$ref' together with inline 'bearerToken' in 'oauth2authserver'."); + } + + @Test + public void objectLevelRefTypeMismatchError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + manager: + basicAuthentication: + fileUserDataProvider: + htpasswdPath: /etc/htpasswd + --- + api: + flow: + - oauth2authserver: + issuer: https://issuer + $ref: "#/components/manager" + """)); + assertAnyErrorContains(ex, "Referenced component '#/components/manager' (type 'basicAuthentication') is not allowed in 'oauth2authserver'."); + } + + @Test + public void flowRefTypeMismatchError() { + var ex = assertThrows(RuntimeException.class, () -> parse(""" + components: + manager: + bearerToken: + header: Authorization + --- + api: + flow: + - $ref: "#/components/manager" + """)); + assertAnyErrorContains(ex, "Value of type 'bearerToken' is not allowed in list 'flow'. Expected 'FlowItem'."); + } + + @Test + public void componentRefersToAnotherComponent() { + assertStructure( + parse(""" + components: + manager: + bearerToken: + header: Authorization + oauth1: + oauth2authserver: + issuer: https://issuer + otherFields: abc + $ref: "#/components/manager" + --- + api: + flow: + - $ref: "#/components/oauth1" + """), + clazz("Components"), + clazz("ApiElement", + property("flow", list( + clazz("OAuth2AuthServerElement", + property("issuer", value("https://issuer")), + property("otherFields", value("abc")), + property("bearerToken", + clazz("BearerTokenElement", + property("header", value("Authorization"))))) + ))) + ); + } + + @Test + public void topLevelElementNotAllowedAsNestedChild() { + var ex = assertThrows(RuntimeException.class, () -> parseWithTopLevelOnlySources(""" + outer: + items: + - topThing: {} + """)); + assertSchemaErrorContains(ex, "property 'topThing' is not defined in the schema and the schema does not allow additional properties"); + } + + @Test + public void topLevelElementStillAllowedAtRoot() { + assertStructure( + parseWithTopLevelOnlySources(""" + topThing: {} + """), + clazz("TopThingElement") + ); + } + + @Test + public void nonTopLevelElementAllowedAsNestedChild() { + assertStructure( + parseWithTopLevelOnlySources(""" + outer: + items: + - inner: {} + """), + clazz("OuterElement", + property("items", list( + clazz("InnerElement") + ))) + ); + } + + private BeanRegistry parseWithTopLevelOnlySources(String yaml) { + var sources = splitSources(MC_MAIN_DEMO + COMPONENTS_DEMO_SOURCES + TOPLEVEL_ONLY_SOURCES); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + return parseYAML(result, yaml); + } + + private static final String TOPLEVEL_ONLY_SOURCES = """ + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + + @MCElement(name="outer", topLevel=true) + public class OuterElement { + List items; + + public List getItems() { + return items; + } + + @MCChildElement + public void setItems(List items) { + this.items = items; + } + } + --- + package com.predic8.membrane.demo; + public abstract class ItemBase {} + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="inner") + public class InnerElement extends ItemBase {} + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="topThing", topLevel=true) + public class TopThingElement extends ItemBase {} + """; + + + private List parseDocs(String yamlWithDocs) { + var sources = splitSources(MC_MAIN_DEMO + COMPONENTS_DEMO_SOURCES); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + return splitYamlDocs(yamlWithDocs).stream() + .map(doc -> parseYAML(result, doc)) + .flatMap(r -> r.getBeans().stream()) + .toList(); + } + + private List splitYamlDocs(String yaml) { + return java.util.regex.Pattern.compile("(?m)^---\\s*$") + .splitAsStream(yaml) + .map(String::trim) + .filter(s -> !s.isBlank()) + .toList(); + } + + private BeanRegistry parse(String yaml) { + var sources = splitSources(MC_MAIN_DEMO + COMPONENTS_DEMO_SOURCES); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + return parseYAML(result, yaml); + } + + private void assertSchemaErrorContains(RuntimeException ex, String... needles) { + var root = getCause(ex); + if (!(root instanceof YamlSchemaValidationException yse)) + throw new AssertionError("Expected YamlSchemaValidationException but got: " + root, root); + + assertFalse(yse.getErrors().isEmpty(), "Expected schema errors."); + var msg = yse.getErrors().getFirst().toString(); + for (var n : needles) + assertTrue(msg.contains(n), () -> "Expected error to contain '" + n + "' but was: " + msg); + } + + private void assertAnyErrorContains(RuntimeException ex, String... needles) { + var root = getCause(ex); + var msg = root.getMessage() != null ? root.getMessage() : root.toString(); + for (var n : needles) + assertTrue(msg.toLowerCase().contains(n.toLowerCase()), () -> "Expected error to contain '" + n + "' but was: " + msg); + } + + private Throwable getCause(Throwable e) { + if (e.getCause() != null) + return getCause(e.getCause()); + return e; + } + + private static final String COMPONENTS_DEMO_SOURCES = """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + + @MCElement(name="api", topLevel=true) + public class ApiElement { + List flow; + + public List getFlow() { + return flow; + } + + @MCChildElement + public void setFlow(List flow) { + this.flow = flow; + } + } + --- + package com.predic8.membrane.demo; + public abstract class FlowItem {} + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="basicAuthentication") + public class BasicAuthenticationElement extends FlowItem { + FileUserDataProviderElement fileUserDataProvider; + + public FileUserDataProviderElement getFileUserDataProvider() { + return fileUserDataProvider; + } + + @MCChildElement + public void setFileUserDataProvider(FileUserDataProviderElement fileUserDataProvider) { + this.fileUserDataProvider = fileUserDataProvider; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="fileUserDataProvider", component=false) + public class FileUserDataProviderElement { + String htpasswdPath; + + public String getHtpasswdPath() { + return htpasswdPath; + } + + @MCAttribute + public void setHtpasswdPath(String htpasswdPath) { + this.htpasswdPath = htpasswdPath; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="template") + public class TemplateElement extends FlowItem { + String location; + + public String getLocation() { + return location; + } + + @MCAttribute + public void setLocation(String location) { + this.location = location; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="oauth2authserver") + public class OAuth2AuthServerElement extends FlowItem { + String issuer; + String otherFields; + BearerTokenElement bearerToken; + + public String getIssuer() { + return issuer; + } + + @MCAttribute + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getOtherFields() { + return otherFields; + } + + @MCAttribute + public void setOtherFields(String otherFields) { + this.otherFields = otherFields; + } + + public BearerTokenElement getBearerToken() { + return bearerToken; + } + + @MCChildElement + public void setBearerToken(BearerTokenElement bearerToken) { + this.bearerToken = bearerToken; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="bearerToken") + public class BearerTokenElement { + String header; + + public String getHeader() { + return header; + } + + @MCAttribute + public void setHeader(String header) { + this.header = header; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="demoBean") + public class BeanElement { + } + """; +} diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java index 9ac3599ac5..25b7127611 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java @@ -33,7 +33,7 @@ public void simple() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { } """); @@ -54,7 +54,7 @@ public void attribute() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { public String attr; @@ -87,7 +87,7 @@ public void singleChild() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { Child1Element child; @@ -103,7 +103,7 @@ public void setChild(Child1Element child) { --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child1", topLevel=false) + @MCElement(name="child1", component=false) public class Child1Element { } """); @@ -126,7 +126,7 @@ public void twoObjects() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { } """); @@ -151,7 +151,7 @@ public void nestedChildren() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { Child1Element child; @@ -167,7 +167,7 @@ public void setChild(Child1Element child) { --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child1", topLevel=false) + @MCElement(name="child1", component=false) public class Child1Element { Child2Element child; @@ -183,7 +183,7 @@ public void setChild(Child2Element child) { --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child2", topLevel=false) + @MCElement(name="child2", component=false) public class Child2Element { } """); @@ -207,7 +207,7 @@ public void nestedListOfChildsWithAttr() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { Child1Element child; @@ -237,7 +237,7 @@ public void setChild(List child) { this.child = child; } - @MCElement(name="child2", topLevel=false) + @MCElement(name="child2", component=false) public static class Child2Element { public String attr; @@ -270,13 +270,69 @@ public void setAttr(String attr) { property("attr", value("here"))))))))); } + @Test + public void noEnvelope() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="demo", noEnvelope=true, topLevel=true) + public class DemoElement { + List children; + + public List getChildren() { + return children; + } + + @MCChildElement + public void setChildren(List children) { + this.children = children; + } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="child1") + public class Child1Element { + public String attr; + + public String getAttr() { + return attr; + } + + @MCAttribute + public void setAttr(String attr) { + this.attr = attr; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + assertStructure( + parseYAML(result, """ + demo: + - child1: + attr: here + - child1: + attr: here2 + """), + clazz("DemoElement", + property("children", list( + clazz("Child1Element", + property("attr", value("here"))), + clazz("Child1Element", + property("attr", value("here2"))))))); + } + @Test public void nestedListOfChildsWithAttr2() { var sources = splitSources(MC_MAIN_DEMO + """ package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="outer") + @MCElement(name="outer", topLevel=true) public class OuterElement { List child; @@ -323,7 +379,7 @@ public void setChild(List child) { this.child = child; } - @MCElement(name="child2", mixed=true, topLevel=false) + @MCElement(name="child2", mixed=true, component=false) public static class Child2Element { public String attr; public String content; @@ -377,7 +433,7 @@ public void errorInSecondLevelWord() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { Child1Element child; @@ -393,13 +449,13 @@ public void setChild(Child1Element child) { --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child1", topLevel=false) + @MCElement(name="child1", component=false) public class Child1Element { } --- package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="demo2") + @MCElement(name="demo2", topLevel=true) public class Demo2Element { } --- @@ -427,7 +483,7 @@ public void requiredChild() { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; import java.util.List; - @MCElement(name="demo") + @MCElement(name="demo", topLevel=true) public class DemoElement { ChildElement child; @@ -475,7 +531,6 @@ public class Child2Element extends ChildElement { } } - @Disabled("This test currently fails, but because of the wrong reason. Disabling it.") @Test public void errorInListItemUniqueness() { var sources = splitSources(MC_MAIN_DEMO + """ @@ -533,7 +588,7 @@ public class Demo2Element { } catch (RuntimeException e) { YamlSchemaValidationException e2 = (YamlSchemaValidationException) getCause(e); assertEquals(1, e2.getErrors().size()); - assertEquals("/demo: property 'errorHere' is not defined in the schema and the schema does not allow additional properties", + assertEquals(": property 'demo' is not defined in the schema and the schema does not allow additional properties", e2.getErrors().getFirst().toString()); } } diff --git a/annot/src/test/java/com/predic8/membrane/annot/YamlSetterConflictTest.java b/annot/src/test/java/com/predic8/membrane/annot/YamlSetterConflictTest.java index 75346fcf28..990931ae7b 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YamlSetterConflictTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YamlSetterConflictTest.java @@ -42,7 +42,7 @@ public void setE(List s) {} package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="b", topLevel = false, id = "b") + @MCElement(name="b", component = false, id = "b") public class B { } """); @@ -81,7 +81,7 @@ public abstract class AbstractF { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractC; - @MCElement(name="d", topLevel = false, id = "d1") + @MCElement(name="d", component = false, id = "d1") public class D extends AbstractC { } --- @@ -89,7 +89,7 @@ public class D extends AbstractC { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractF; - @MCElement(name="d", topLevel = false, id = "d2") + @MCElement(name="d", component = false, id = "d2") public class D extends AbstractF { } """); @@ -122,7 +122,7 @@ public abstract class AbstractChild { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child", topLevel = false, id = "child") + @MCElement(name="child", component = false, id = "child") public class ConcreteChild extends AbstractChild { } """); @@ -152,7 +152,7 @@ public abstract class AbstractChildElement { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractChildElement; - @MCElement(name="child", topLevel = false, id = "child1") + @MCElement(name="child", component = false, id = "child1") public class ChildA extends AbstractChildElement { } --- @@ -160,7 +160,7 @@ public class ChildA extends AbstractChildElement { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractChildElement; - @MCElement(name="child", topLevel = false, id = "child2") + @MCElement(name="child", component = false, id = "child2") public class ChildB extends AbstractChildElement { } """); @@ -187,7 +187,7 @@ public void setE(B b) {} package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="b", topLevel = false, id = "b") + @MCElement(name="b", component = false, id = "b") public class B { } """); @@ -225,7 +225,7 @@ public abstract class AbstractF { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractC; - @MCElement(name="d", topLevel = false, id = "d1") + @MCElement(name="d", component = false, id = "d1") public class DFromC extends AbstractC { } --- @@ -233,7 +233,7 @@ public class DFromC extends AbstractC { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractF; - @MCElement(name="d", topLevel = false, id = "d2") + @MCElement(name="d", component = false, id = "d2") public class DFromF extends AbstractF { } """); @@ -265,7 +265,7 @@ public abstract class AbstractChild { package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="child", topLevel = false, id = "child") + @MCElement(name="child", component = false, id = "child") public class ConcreteChild extends AbstractChild { } """); @@ -295,7 +295,7 @@ public abstract class AbstractChildElement { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractChildElement; - @MCElement(name="child", topLevel = false, id = "child1") + @MCElement(name="child", component = false, id = "child1") public class ChildA extends AbstractChildElement { } --- @@ -303,7 +303,7 @@ public class ChildA extends AbstractChildElement { import com.predic8.membrane.annot.*; import com.predic8.membrane.demo.AbstractChildElement; - @MCElement(name="child", topLevel = false, id = "child2") + @MCElement(name="child", component = false, id = "child2") public class ChildB extends AbstractChildElement { } """); @@ -332,7 +332,7 @@ public void setB(Child c) {} package com.predic8.membrane.demo; import com.predic8.membrane.annot.*; - @MCElement(name="a", topLevel = false, id = "child") + @MCElement(name="a", component = false, id = "child") public class Child { } """); diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java index ba0078b7da..4f417bbe34 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java +++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java @@ -28,8 +28,7 @@ import static java.util.List.*; import static java.util.stream.StreamSupport.*; -import static javax.tools.Diagnostic.Kind.ERROR; -import static javax.tools.Diagnostic.Kind.WARNING; +import static javax.tools.Diagnostic.Kind.*; import static javax.tools.StandardLocation.*; import static org.hamcrest.MatcherAssert.*; import static org.junit.jupiter.api.Assertions.*; @@ -37,10 +36,10 @@ public class CompilerHelper { public static final String YAML_PARSER_CLASS_NAME = "com.predic8.membrane.annot.util.YamlParser"; - private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+([^;]+)\\s*;"); - private static final Pattern CLASS_PATTERN = Pattern.compile("class\\s+([^\\s]+)\\s"); public static final String ANNOTATION_PROCESSOR_CLASSNAME = "com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessor"; public static final String APPLICATION_CONTEXT_CLASSNAME = "org.springframework.context.support.ClassPathXmlApplicationContext"; + private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s+([^;]+)\\s*;"); + private static final Pattern CLASS_PATTERN = Pattern.compile("class\\s+([^\\s]+)\\s"); /** * Compile the given source files. @@ -49,7 +48,6 @@ public class CompilerHelper { * @param logCompilerOutput if true, print the compiler output to stderr */ public static CompilerResult compile(Iterable sourceFiles, boolean logCompilerOutput) { - var javaSources = getJavaSources(sourceFiles); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { throw new IllegalStateException("No system Java compiler found. Run tests with a JDK, not a JRE."); @@ -65,7 +63,7 @@ public static CompilerResult compile(Iterable sourceFiles, diagnostics, of("-processor", ANNOTATION_PROCESSOR_CLASSNAME), null, - javaSources + getJavaSources(sourceFiles) ); boolean success = task.call(); @@ -77,12 +75,34 @@ public static CompilerResult compile(Iterable sourceFiles, } public static BeanRegistry parseYAML(CompilerResult cr, String yamlConfig) { - ClassLoader original = Thread.currentThread().getContextClassLoader(); CompositeClassLoader cl = getCompositeClassLoader(cr, yamlConfig); + return withContextClassLoader(cl, () -> { + Class parserClass = cl.loadClass(YAML_PARSER_CLASS_NAME); + return getBeanRegistry(parserClass, getParser(parserClass)); + }); + } + + public static void parseXML(CompilerResult cr, String xmlSpringConfig) { + CompositeClassLoader cl = xmlClassLoader(cr, xmlSpringConfig); + withContextClassLoader(cl, () -> { + Class ctx = cl.loadClass(APPLICATION_CONTEXT_CLASSNAME); + Object context = ctx.getConstructor(String.class).newInstance("demo.xml"); + try { + // Context successfully created - validation passed + } finally { + ctx.getMethod("close").invoke(context); + } + return null; + }); + } + + private static T withContextClassLoader(ClassLoader cl, ThrowingSupplier action) { + ClassLoader original = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(cl); - Class parserClass = cl.loadClass(YAML_PARSER_CLASS_NAME); - return getBeanRegistry(parserClass,getParser(parserClass)); + return action.get(); + } catch (RuntimeException | Error e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } finally { @@ -96,34 +116,22 @@ private static BeanRegistry getBeanRegistry(Class parserClass, Object instanc .invoke(instance); } - private static @NotNull CompositeClassLoader getCompositeClassLoader(CompilerResult cr, String yamlConfig) { - InMemoryClassLoader loaderA = (InMemoryClassLoader) cr.classLoader(); - loaderA.defineOverlay(new OverlayInMemoryFile("/demo.yaml", yamlConfig)); - return new CompositeClassLoader(CompilerHelper.class.getClassLoader(), loaderA); + private static CompositeClassLoader getCompositeClassLoader(CompilerResult cr, String yamlConfig) { + return overlayClassLoader(cr, yamlConfig, "/demo.yaml"); } - private static @NotNull Object getParser(Class c) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { - return c.getConstructor(String.class).newInstance("demo.yaml"); + private static CompositeClassLoader xmlClassLoader(CompilerResult cr, String xmlSpringConfig) { + return overlayClassLoader(cr, xmlSpringConfig, "/demo.xml"); } - /** - * Parse the given XML Spring config. - * TODO Refactor: too much in common with parseYAML - */ - public static void parse(CompilerResult cr, String xmlSpringConfig) { - ClassLoader originalClassloader = Thread.currentThread().getContextClassLoader(); - try { - InMemoryClassLoader loaderA = (InMemoryClassLoader) cr.classLoader(); - loaderA.defineOverlay(new OverlayInMemoryFile("/demo.xml", xmlSpringConfig)); - CompositeClassLoader cl = new CompositeClassLoader(CompilerHelper.class.getClassLoader(),loaderA); - Thread.currentThread().setContextClassLoader(cl); - Class c = cl.loadClass(APPLICATION_CONTEXT_CLASSNAME); - c.getConstructor(String.class).newInstance("demo.xml"); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - Thread.currentThread().setContextClassLoader(originalClassloader); - } + private static CompositeClassLoader overlayClassLoader(CompilerResult cr, String content, String resourcePath) { + InMemoryClassLoader inMemory = (InMemoryClassLoader) cr.classLoader(); + inMemory.defineOverlay(new OverlayInMemoryFile(resourcePath, content)); + return new CompositeClassLoader(CompilerHelper.class.getClassLoader(), inMemory); + } + + private static @NotNull Object getParser(Class c) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + return c.getConstructor(String.class).newInstance("demo.yaml"); } private static List getJavaSources(Iterable sources) { @@ -164,7 +172,12 @@ private static FileObject toFile(String content) { if (!content.trim().startsWith("resource")) return toInMemoryJavaFile(content); - // TODO extract method + String[] parts = stripFirstLine(content); + + return new OverlayInMemoryFile(parts[0].substring("resource".length()).trim(), parts[1]); + } + + static String @NotNull [] stripFirstLine(String content) { String[] parts; while (true) { parts = content.split("\n", 2); @@ -174,9 +187,7 @@ private static FileObject toFile(String content) { break; content = parts[1]; } - - String name = parts[0].substring(9).trim(); // TODO Refactor and give meaningful name - return new OverlayInMemoryFile(name, parts[1]); + return parts; } private static JavaFileObject toInMemoryJavaFile(String source) { @@ -236,4 +247,9 @@ public boolean matches(Object o) { } }; } + + @FunctionalInterface + private interface ThrowingSupplier { + T get() throws Exception; + } } diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java b/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java new file mode 100644 index 0000000000..a62f428860 --- /dev/null +++ b/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java @@ -0,0 +1,41 @@ +package com.predic8.membrane.annot.util; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +class ReflectionUtilTest { + + + @Test + void convertString() { + var o = ReflectionUtil.convert("abc", String.class); + assertInstanceOf(String.class, o); + assertEquals("abc", o); + } + + @Test + void convertInteger() { + var o = ReflectionUtil.convert("123", Integer.class); + assertInstanceOf(Integer.class, o); + assertEquals(123, o); + } + + @Test + void convertBoolean() { + var o = ReflectionUtil.convert("true", Boolean.class); + assertInstanceOf(Boolean.class, o); + assertEquals(true, o); + } + + @Test + void convertNull() { + var o = ReflectionUtil.convert(null, String.class); + assertNull(o); + } + + @Test + void convertInvalid() { + assertThrows(RuntimeException.class, () -> ReflectionUtil.convert("abc", Integer.class)); + } +} \ No newline at end of file diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/StructureAssertionUtil.java b/annot/src/test/java/com/predic8/membrane/annot/util/StructureAssertionUtil.java index ea1393edb8..eaf451eaab 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/util/StructureAssertionUtil.java +++ b/annot/src/test/java/com/predic8/membrane/annot/util/StructureAssertionUtil.java @@ -24,10 +24,43 @@ public class StructureAssertionUtil { public static void assertStructure(BeanRegistry registry, Asserter... asserter) { - assertEquals(registry.getBeans().size(), asserter.length); - for (int i = 0; i < asserter.length; i++) { - asserter[i].assertStructure(registry.getBeans().get(i)); + assertStructure(registry.getBeans(), asserter); + } + + public static void assertStructure(List beans, Asserter... asserter) { + assertEquals(asserter.length, beans.size()); + + boolean[] used = new boolean[beans.size()]; + AssertionError failure = matchAnyOrder(beans, asserter, used, 0); + + if (failure != null) throw failure; + } + + private static AssertionError matchAnyOrder(List beans, Asserter[] expected, boolean[] used, int idx) { + if (idx == expected.length) return null; + + AssertionError last = null; + + for (int i = 0; i < beans.size(); i++) { + if (used[i]) continue; + + try { + expected[idx].assertStructure(beans.get(i)); + used[i] = true; + + AssertionError res = matchAnyOrder(beans, expected, used, idx + 1); + if (res == null) return null; + + used[i] = false; + last = res; + } catch (AssertionError e) { + last = e; + } catch (RuntimeException e) { + last = new AssertionError(e.getMessage(), e); + } } + + return last != null ? last : new AssertionError("No matching bean found for expected index " + idx); } public interface Asserter { @@ -40,7 +73,7 @@ public interface Property { public static Asserter clazz(String clazzName, Property... properties) { return bean -> { - assertEquals(bean.getClass().getSimpleName(), clazzName); + assertEquals(clazzName, bean.getClass().getSimpleName()); for (Property p : properties) { p.assertStructure(bean); } @@ -55,7 +88,7 @@ public static Asserter list(Asserter... asserters) { return bean -> { assertInstanceOf(List.class, bean); List list = (List) bean; - assertEquals(list.size(), asserters.length); + assertEquals(asserters.length, list.size()); for (int i = 0; i < asserters.length; i++) { asserters[i].assertStructure(list.get(i)); } diff --git a/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java b/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java index 7ae32a4cac..3ecc122f92 100644 --- a/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java +++ b/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java @@ -21,7 +21,7 @@ * @description Configures Azure DNS for ACME DNS-01 validation. * @topic 8. ACME */ -@MCElement(topLevel = false, name = "azureDns") +@MCElement(component = false, name = "azureDns") public class AzureDns extends AcmeValidation { private String dnsZoneName; diff --git a/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java b/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java index e34c603b86..3d60200705 100644 --- a/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java +++ b/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java @@ -19,7 +19,7 @@ import com.predic8.membrane.core.config.security.acme.AcmeSynchronizedStorage; import com.predic8.membrane.core.transport.http.client.HttpClientConfiguration; -@MCElement(name = "azureTableStorage", topLevel = false) +@MCElement(name = "azureTableStorage", component = false) public class AzureTableStorage implements AcmeSynchronizedStorage { private String storageAccountName; diff --git a/core/src/main/java/com/predic8/membrane/core/config/Path.java b/core/src/main/java/com/predic8/membrane/core/config/Path.java index a73052ff2f..60db090819 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/Path.java +++ b/core/src/main/java/com/predic8/membrane/core/config/Path.java @@ -40,7 +40,7 @@ * available in scripts via the pathParam variable. *

*/ -@MCElement(name="path", topLevel=false, mixed=true) +@MCElement(name="path", component =false, mixed=true) public class Path { private String uri; diff --git a/core/src/main/java/com/predic8/membrane/core/config/security/acme/DnsOperatorAcmeValidation.java b/core/src/main/java/com/predic8/membrane/core/config/security/acme/DnsOperatorAcmeValidation.java index 110192e3cf..ab5bbd2622 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/security/acme/DnsOperatorAcmeValidation.java +++ b/core/src/main/java/com/predic8/membrane/core/config/security/acme/DnsOperatorAcmeValidation.java @@ -15,7 +15,7 @@ import com.predic8.membrane.annot.MCElement; -@MCElement(topLevel = false, name = "dnsOperator") +@MCElement(component = false, name = "dnsOperator") public class DnsOperatorAcmeValidation extends AcmeValidation { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/config/security/acme/FileStorage.java b/core/src/main/java/com/predic8/membrane/core/config/security/acme/FileStorage.java index eca973641d..d69d09dc4f 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/security/acme/FileStorage.java +++ b/core/src/main/java/com/predic8/membrane/core/config/security/acme/FileStorage.java @@ -18,7 +18,7 @@ import java.util.Objects; -@MCElement(name = "fileStorage", topLevel = false) +@MCElement(name = "fileStorage", component = false) public class FileStorage implements AcmeSynchronizedStorage { String dir; diff --git a/core/src/main/java/com/predic8/membrane/core/config/security/acme/KubernetesStorage.java b/core/src/main/java/com/predic8/membrane/core/config/security/acme/KubernetesStorage.java index b9b99ebfee..0a5c060c35 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/security/acme/KubernetesStorage.java +++ b/core/src/main/java/com/predic8/membrane/core/config/security/acme/KubernetesStorage.java @@ -19,7 +19,7 @@ import java.util.Objects; -@MCElement(name = "kubernetesStorage", topLevel = false) +@MCElement(name = "kubernetesStorage", component = false) public class KubernetesStorage implements AcmeSynchronizedStorage { String baseURL; String namespace; diff --git a/core/src/main/java/com/predic8/membrane/core/config/security/acme/MemoryStorage.java b/core/src/main/java/com/predic8/membrane/core/config/security/acme/MemoryStorage.java index 4828d460d3..1a9cf55976 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/security/acme/MemoryStorage.java +++ b/core/src/main/java/com/predic8/membrane/core/config/security/acme/MemoryStorage.java @@ -19,7 +19,7 @@ * @description * For testing purposes only. */ -@MCElement(name = "memoryStorage", topLevel = false) +@MCElement(name = "memoryStorage", component = false) public class MemoryStorage implements AcmeSynchronizedStorage { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java index 75bf5ddc77..7292f8b4d9 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java @@ -21,7 +21,7 @@ import static javax.xml.XMLConstants.NULL_NS_URI; -@MCElement(name="namespaces", topLevel = false) +@MCElement(name="namespaces", component = false) public class Namespaces { private List namespaces; @@ -43,7 +43,7 @@ public List getNamespace() { return namespaces; } - @MCElement(name = "namespace", topLevel = false, id = "xml-namespace") + @MCElement(name = "namespace", component = false, id = "xml-namespace") public static class Namespace { public String prefix; diff --git a/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java b/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java index dd4da4024e..5d9c0f0069 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java @@ -16,7 +16,7 @@ import com.predic8.membrane.annot.*; -@MCElement(name="xmlConfig",topLevel = true) +@MCElement(name="xmlConfig", component = true) public class XmlConfig { private Namespaces namespaces; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java index fc56d0e5b5..1ced96ac4a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/DispatchingInterceptor.java @@ -41,7 +41,7 @@ * object. The dispatching interceptor needs the service proxy to * get information about the target. */ -@MCElement(name = "dispatching") +@MCElement(name = "dispatching", excludeFromFlow = true) public class DispatchingInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(DispatchingInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/EchoInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/EchoInterceptor.java index f2a2dc2f4d..3d109cfdf2 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/EchoInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/EchoInterceptor.java @@ -26,7 +26,7 @@ * request into a new response. The response has a status code of 200. * Useful for testing. */ -@MCElement(name="echo", topLevel = false) +@MCElement(name="echo", component = false) public class EchoInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(EchoInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/ExchangeStoreInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ExchangeStoreInterceptor.java index 577d264d4b..0de4c77b33 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/ExchangeStoreInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/ExchangeStoreInterceptor.java @@ -36,7 +36,7 @@ * might both be required for the exchange to be saved. * @topic 4. Monitoring, Logging and Statistics */ -@MCElement(name="exchangeStore") +@MCElement(name="exchangeStore", excludeFromFlow = true) public class ExchangeStoreInterceptor extends AbstractInterceptor implements ApplicationContextAware { private static final String BEAN_ID_ATTRIBUTE_CANNOT_BE_USED = "bean id attribute cannot be used"; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/GlobalInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/GlobalInterceptor.java index b33a848ebf..476def64d3 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/GlobalInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/GlobalInterceptor.java @@ -22,7 +22,7 @@ * @description The global chain applies plugins to all endpoints, enabling centralized features * such as global user authentication, logging, and other cross-cutting concerns. */ -@MCElement(name = "global") +@MCElement(name = "global", excludeFromFlow = true) public class GlobalInterceptor extends AbstractFlowWithChildrenInterceptor { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java index 4260021623..4496d1d1bd 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/HTTPClientInterceptor.java @@ -40,7 +40,7 @@ * its outgoing HTTP connection that is different from the global * configuration in the transport. */ -@MCElement(name = "httpClient") +@MCElement(name = "httpClient", excludeFromFlow= true) public class HTTPClientInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(HTTPClientInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/RuleMatchingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/RuleMatchingInterceptor.java index d02258b045..ac6da7eb92 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/RuleMatchingInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/RuleMatchingInterceptor.java @@ -28,7 +28,7 @@ import static com.predic8.membrane.core.interceptor.Outcome.*; @SuppressWarnings("unused") -@MCElement(name="ruleMatching") +@MCElement(name="ruleMatching", excludeFromFlow = true) public class RuleMatchingInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(RuleMatchingInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/UserFeatureInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/UserFeatureInterceptor.java index 50580ac007..28721aec26 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/UserFeatureInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/UserFeatureInterceptor.java @@ -22,7 +22,7 @@ /** * Handles features that are user-configured in proxies.xml . */ -@MCElement(name="userFeature") +@MCElement(name="userFeature", excludeFromFlow = true) public class UserFeatureInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(UserFeatureInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 99504b1eef..67ecb950c7 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -36,7 +36,7 @@ * @description

The wsdlRewriter rewrites endpoint addresses of services and XML Schema locations in WSDL documents.

* @topic 5. Web Services with SOAP and WSDL */ -@MCElement(name = "wsdlRewriter") +@MCElement(name = "wsdlRewriter", excludeFromFlow = true) public class WSDLInterceptor extends RelocatingInterceptor { private final static Logger log = LoggerFactory.getLogger(WSDLInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/addHeader.java b/core/src/main/java/com/predic8/membrane/core/interceptor/addHeader.java index 696cb6ef01..434cffafe8 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/addHeader.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/addHeader.java @@ -17,7 +17,7 @@ import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.core.http.HeaderField; -@MCElement(name = "addHeader", topLevel = false) +@MCElement(name = "addHeader", component = false) public class addHeader { private String name; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/adminapi/AdminApiInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/adminapi/AdminApiInterceptor.java index 6336e070ed..4eef0a2d13 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/adminapi/AdminApiInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/adminapi/AdminApiInterceptor.java @@ -53,7 +53,7 @@ import static java.net.URLDecoder.decode; import static java.nio.charset.StandardCharsets.UTF_8; -@MCElement(name = "adminApi") +@MCElement(name = "adminApi", excludeFromFlow = true) public class AdminApiInterceptor extends AbstractInterceptor { static final DateTimeFormatter isoFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java index 4a1ae88d9e..13063d7188 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java @@ -47,7 +47,7 @@ *

* @topic 3. Security and Validation */ -@MCElement(name="expressionExtractor", topLevel = false) +@MCElement(name="expressionExtractor", component = false) public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XMLSupport { private String expression = ""; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java index 42b7b16db8..1f2a82bd94 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java @@ -36,7 +36,7 @@ * * @topic 3. Security and Validation */ -@MCElement(name="headerExtractor", topLevel = false) +@MCElement(name="headerExtractor", component = false) public class ApiKeyHeaderExtractor implements ApiKeyExtractor{ private HeaderName headerName = new HeaderName("X-Api-Key"); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyQueryParamExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyQueryParamExtractor.java index ed44c59ea4..271956ad80 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyQueryParamExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyQueryParamExtractor.java @@ -41,7 +41,7 @@ * * @topic 3. Security and Validation */ -@MCElement(name="queryParamExtractor", topLevel = false) +@MCElement(name="queryParamExtractor", component = false) public class ApiKeyQueryParamExtractor implements ApiKeyExtractor{ private static final Logger log = LoggerFactory.getLogger(ApiKeyQueryParamExtractor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Key.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Key.java index d6bf186672..bfb5804cbd 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Key.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Key.java @@ -23,7 +23,7 @@ /** * @description Contains api keys and scopes. */ -@MCElement(name = "secret", topLevel = false) +@MCElement(name = "secret", component = false) public class Key { private final List scopes = new ArrayList<>(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Scope.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Scope.java index d38e22ea69..cf65e1dca6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Scope.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Scope.java @@ -19,7 +19,7 @@ /** * @description Contains a scope for use in ... elements. */ -@MCElement(name = "scope", topLevel = false, mixed = true) +@MCElement(name = "scope", component = false, mixed = true) public class Scope { private String value; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/SimpleKeyStore.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/SimpleKeyStore.java index 1fde1bb65c..2ebda0fa12 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/SimpleKeyStore.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/SimpleKeyStore.java @@ -24,7 +24,7 @@ /** * @description Stores api keys inline as XML. */ -@MCElement(name = "keys", topLevel = false, noEnvelope = true) +@MCElement(name = "keys", component = false, noEnvelope = true) public class SimpleKeyStore implements ApiKeyStore { private final List keys = new ArrayList<>(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/CachingUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/CachingUserDataProvider.java index 0c22c0a8f8..c0e81664e2 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/CachingUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/CachingUserDataProvider.java @@ -31,7 +31,7 @@ /** * @description Caching User Data provider caches previous successful logins in order to make authentication faster */ -@MCElement(name="cachingUserDataProvider", topLevel=false) +@MCElement(name="cachingUserDataProvider", component =false) public class CachingUserDataProvider implements UserDataProvider { private static final Logger log = LoggerFactory.getLogger(CachingUserDataProvider.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmailTokenProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmailTokenProvider.java index ff35c19b99..a0518b4b6f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmailTokenProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmailTokenProvider.java @@ -51,7 +51,7 @@ * will be replaced by the numeric token value. *

*/ -@MCElement(name="emailTokenProvider", topLevel=false) +@MCElement(name="emailTokenProvider", component =false) public class EmailTokenProvider extends NumericTokenProvider { private static final Logger log = LoggerFactory.getLogger(EmailTokenProvider.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmptyTokenProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmptyTokenProvider.java index 707a7f950a..540bd943e6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmptyTokenProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/EmptyTokenProvider.java @@ -19,7 +19,7 @@ import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.core.Router; -@MCElement(name="emptyTokenProvider", topLevel=false) +@MCElement(name="emptyTokenProvider", component =false) public class EmptyTokenProvider implements TokenProvider { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/LDAPUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/LDAPUserDataProvider.java index 1e0d870226..2b78fd9319 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/LDAPUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/LDAPUserDataProvider.java @@ -91,7 +91,7 @@ * attributes. *

*/ -@MCElement(name="ldapUserDataProvider", topLevel=false) +@MCElement(name="ldapUserDataProvider", component =false) public class LDAPUserDataProvider implements UserDataProvider { private static final Logger log = LoggerFactory.getLogger(LDAPUserDataProvider.class.getName()); @@ -110,10 +110,10 @@ public class LDAPUserDataProvider implements UserDataProvider { AttributeMap map; SSLParser sslParser; - @MCElement(name="map", topLevel=false, id="ldapUserDataProvider-map", noEnvelope = true) + @MCElement(name="map", component =false, id="ldapUserDataProvider-map", noEnvelope = true) public static class AttributeMap { - @MCElement(name="attribute", topLevel=false) + @MCElement(name="attribute", component =false) public static class Attribute { String from; String to; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java index 8e9efd8d0c..3a2a4cfb55 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java @@ -39,7 +39,7 @@ * timeout is 5 minutes. *

*/ -@MCElement(name="sessionManager", topLevel=false) +@MCElement(name="sessionManager", component =false) public class SessionManager extends AbstractXmlElement implements Cleaner { private String cookieName; private long timeout; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java index 087e10bc8f..3331e8e2be 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/StaticUserDataProvider.java @@ -96,7 +96,7 @@ private String createPasswdCompatibleHash(String algo, String password, String s return Crypt.crypt(password, "$" + algo + "$" + salt); } - @MCElement(name="user", topLevel=false, id="staticUserDataProvider-user") + @MCElement(name="user", component =false, id="staticUserDataProvider-user") public static class User { final Map attributes = new HashMap<>(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TOTPTokenProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TOTPTokenProvider.java index 8fcdab8220..f18b1c2833 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TOTPTokenProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TOTPTokenProvider.java @@ -44,7 +44,7 @@ * Authenticator App to store the pre-shared secret and generate such tokens. *

*/ -@MCElement(name="totpTokenProvider", topLevel=false) +@MCElement(name="totpTokenProvider", component =false) public class TOTPTokenProvider implements TokenProvider { final Logger log = LoggerFactory.getLogger(TOTPTokenProvider.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TelekomSMSTokenProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TelekomSMSTokenProvider.java index 193d1df521..40211a7bd3 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TelekomSMSTokenProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/TelekomSMSTokenProvider.java @@ -71,7 +71,7 @@ * *

*/ -@MCElement(name="telekomSMSTokenProvider", topLevel=false) +@MCElement(name="telekomSMSTokenProvider", component =false) public class TelekomSMSTokenProvider extends SMSTokenProvider { private static final Logger log = LoggerFactory.getLogger(TelekomSMSTokenProvider.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/UnifyingUserDataProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/UnifyingUserDataProvider.java index fd014f4154..329726654a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/UnifyingUserDataProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/UnifyingUserDataProvider.java @@ -37,7 +37,7 @@ * provider could verify the user, the login attempt fails. *

*/ -@MCElement(name="unifyingUserDataProvider", topLevel=false, noEnvelope = true) +@MCElement(name="unifyingUserDataProvider", component =false, noEnvelope = true) public class UnifyingUserDataProvider implements UserDataProvider { private List userDataProviders = new ArrayList<>(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/WhateverMobileSMSTokenProvider.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/WhateverMobileSMSTokenProvider.java index ac8ab9ee9e..d69bda4ca7 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/WhateverMobileSMSTokenProvider.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/WhateverMobileSMSTokenProvider.java @@ -51,7 +51,7 @@ * WhateverMobile SMS Gateway. *

*/ -@MCElement(name="whateverMobileSMSTokenProvider", topLevel=false) +@MCElement(name="whateverMobileSMSTokenProvider", component =false) public class WhateverMobileSMSTokenProvider extends SMSTokenProvider { private static final Logger log = LoggerFactory.getLogger(WhateverMobileSMSTokenProvider.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/xen/XenAuthenticationInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/xen/XenAuthenticationInterceptor.java index 26e9f748d1..445e58cedc 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/xen/XenAuthenticationInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/xen/XenAuthenticationInterceptor.java @@ -106,7 +106,7 @@ public interface XenSessionManager { String createSessionId(String xenSessionId); } - @MCElement(name = "inMemorySessionManager", topLevel = false) + @MCElement(name = "inMemorySessionManager", component = false) public static class InMemorySessionManager implements XenSessionManager { private final Map ourSessionIds = new ConcurrentHashMap<>(); private final Map xenSessionIds = new ConcurrentHashMap<>(); @@ -130,7 +130,7 @@ public String createSessionId(String xenSessionId) { } } - @MCElement(name = "jwtSessionManager", topLevel = false, id = "xenAuthentication-jwtSessionManager") + @MCElement(name = "jwtSessionManager", component = false, id = "xenAuthentication-jwtSessionManager") public static class JwtSessionManager implements XenSessionManager { private String audience; @@ -225,7 +225,7 @@ public void setJwk(Jwk jwk) { this.jwk = jwk; } - @MCElement(name="jwk", mixed = true, topLevel = false, id="xenAuthentication-jwtSessionManager-jwk") + @MCElement(name="jwk", mixed = true, component = false, id="xenAuthentication-jwtSessionManager-jwk") public static class Jwk extends Blob { } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Balancer.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Balancer.java index 293bbea953..1e5b09b0a0 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Balancer.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Balancer.java @@ -20,7 +20,7 @@ import java.util.*; -@MCElement(name="clusters", topLevel=false, noEnvelope = true) +@MCElement(name="clusters", component =false, noEnvelope = true) public class Balancer extends AbstractXmlElement { public static final String DEFAULT_NAME = "Default"; private static final Logger log = LoggerFactory.getLogger(Balancer.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Cluster.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Cluster.java index f7d76f7314..4feaa442fb 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Cluster.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Cluster.java @@ -25,7 +25,7 @@ * @description Represents a load-balancing cluster (a named group of {@link Node}s). * Provides status management (UP/DOWN/TAKEOUT), node lookup, and simple session tracking. */ -@MCElement(name="cluster", topLevel=false) +@MCElement(name="cluster", component =false) public class Cluster { private static final Logger log = LoggerFactory.getLogger(Cluster.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Node.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Node.java index 63b33f203a..652edfe8ee 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Node.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/Node.java @@ -34,7 +34,7 @@ * @description Represents a backend node in a load-balancing Cluster. *

Identity is host+port.

*/ -@MCElement(name="node", topLevel=false) +@MCElement(name="node", component =false) public class Node extends AbstractXmlElement { public enum Status { diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/AbortInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/AbortInterceptor.java index ddde447367..52ea5751e0 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/AbortInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/AbortInterceptor.java @@ -29,7 +29,7 @@ * allow normal processing. * @topic 1. Proxies and Flow */ -@MCElement(name="abort", topLevel=false, noEnvelope = true) +@MCElement(name="abort", component =false, noEnvelope = true) public class AbortInterceptor extends AbstractFlowWithChildrenInterceptor { private static final Logger log = LoggerFactory.getLogger(AbortInterceptor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/RequestInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/RequestInterceptor.java index a295eb553d..74ec812ad2 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/RequestInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/RequestInterceptor.java @@ -28,7 +28,7 @@ * <request> Element you can limit their application to requests only. * @topic 1. Proxies and Flow */ -@MCElement(name = "request", topLevel = false, noEnvelope = true) +@MCElement(name = "request", component = false, noEnvelope = true) public class RequestInterceptor extends AbstractFlowWithChildrenInterceptor { private static final Logger log = LoggerFactory.getLogger(RequestInterceptor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ResponseInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ResponseInterceptor.java index b6a34279cd..c6ec24f2d7 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ResponseInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ResponseInterceptor.java @@ -25,7 +25,7 @@ * <response> plugin you can limit their application to responses only. * @topic 1. Proxies and Flow */ -@MCElement(name = "response", topLevel = false, noEnvelope = true) +@MCElement(name = "response", component = false, noEnvelope = true) public class ResponseInterceptor extends AbstractFlowWithChildrenInterceptor { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java index 58cd7dc1d3..696595d495 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java @@ -26,7 +26,7 @@ import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; import static com.predic8.membrane.core.lang.ExchangeExpression.expression; -@MCElement(name = "case", topLevel = false) +@MCElement(name = "case", component = false) public class Case extends InterceptorContainer implements XMLSupport { private static final Logger log = LoggerFactory.getLogger(Case.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Otherwise.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Otherwise.java index 4ec437c5c6..6f7f41aa0b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Otherwise.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Otherwise.java @@ -15,5 +15,5 @@ import com.predic8.membrane.annot.MCElement; -@MCElement(name = "otherwise", topLevel = false, noEnvelope = true) +@MCElement(name = "otherwise", component = false, noEnvelope = true) public class Otherwise extends InterceptorContainer {} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/formvalidation/FormValidationInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/formvalidation/FormValidationInterceptor.java index e18b1db8db..3d5b013879 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/formvalidation/FormValidationInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/formvalidation/FormValidationInterceptor.java @@ -37,7 +37,7 @@ @MCElement(name="formValidation") public class FormValidationInterceptor extends AbstractInterceptor { - @MCElement(name="field", topLevel=false, id="formValidation-field") + @MCElement(name="field", component =false, id="formValidation-field") public static class Field extends AbstractXmlElement { public String name; public String regex; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/grease/strategies/JsonGrease.java b/core/src/main/java/com/predic8/membrane/core/interceptor/grease/strategies/JsonGrease.java index 83b554a6de..9d89dfc5ba 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/grease/strategies/JsonGrease.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/grease/strategies/JsonGrease.java @@ -25,7 +25,7 @@ import static com.predic8.membrane.core.http.MimeType.isJson; -@MCElement(name = "greaseJson", topLevel = false) +@MCElement(name = "greaseJson", component = false) public class JsonGrease extends Greaser { private static final ObjectMapper om = new ObjectMapper(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyTemplateInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyTemplateInterceptor.java index 900593df7e..dda4212813 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyTemplateInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyTemplateInterceptor.java @@ -46,7 +46,7 @@ * * @topic 2. Enterprise Integration Patterns */ -@MCElement(name = "groovyTemplate", mixed = true) +@MCElement(name = "groovyTemplate", mixed = true, excludeFromFlow = true) public class GroovyTemplateInterceptor extends AbstractInterceptor { String src = ""; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java b/core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java index 940ed1868e..42a504ce0a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java @@ -91,7 +91,7 @@ public void setAuthorizationService(AuthorizationService authService) { authorizationService = authService; } - @MCElement(name="jwk", mixed = true, topLevel = false, id="jwks-jwk") + @MCElement(name="jwk", mixed = true, component = false, id="jwks-jwk") public static class Jwk extends Blob { String kid; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/kubernetes/KubernetesValidationInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/kubernetes/KubernetesValidationInterceptor.java index ed3c86f9a6..9349b95bf5 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/kubernetes/KubernetesValidationInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/kubernetes/KubernetesValidationInterceptor.java @@ -142,7 +142,7 @@ * host: thomas-bayer.com * */ -@MCElement(name = "kubernetesValidation") +@MCElement(name = "kubernetesValidation", excludeFromFlow = true) public class KubernetesValidationInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(KubernetesValidationInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/log/AdditionalVariable.java b/core/src/main/java/com/predic8/membrane/core/interceptor/log/AdditionalVariable.java index 0110f4f8bf..fb88a5008b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/log/AdditionalVariable.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/log/AdditionalVariable.java @@ -23,7 +23,7 @@ import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.standard.SpelExpressionParser; -@MCElement(name = "additionalVariable", topLevel = false, id = "accessLog-scope") +@MCElement(name = "additionalVariable", component = false, id = "accessLog-scope") public class AdditionalVariable { private final SpelParserConfiguration spelConfig = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/HeaderNTLMRetriever.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/HeaderNTLMRetriever.java index e560d42f83..e868443d64 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/HeaderNTLMRetriever.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/HeaderNTLMRetriever.java @@ -17,7 +17,7 @@ import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.core.exchange.Exchange; -@MCElement(name = "headerRetriever", topLevel = false) +@MCElement(name = "headerRetriever", component = false) public class HeaderNTLMRetriever implements NTLMRetriever { String userHeaderName; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ClaimList.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ClaimList.java index 834d74553a..685112ae31 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ClaimList.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ClaimList.java @@ -33,7 +33,7 @@ public void setSupportedClaims(HashSet supportedClaims) { this.supportedClaims = supportedClaims; } - @MCElement(name="scope", topLevel=false, id="claims-scope") + @MCElement(name="scope", component =false, id="claims-scope") public static class Scope{ private String id; private String claims; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java index 66e89eb3cc..20088b09e3 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java @@ -17,7 +17,7 @@ import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.annot.Required; -@MCElement(name="client", topLevel=false, id="staticClientList-client") +@MCElement(name="client", component =false, id="staticClientList-client") public class Client { private String clientId; private String clientSecret; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GithubAuthorizationService.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GithubAuthorizationService.java index 4230972779..17e98c7e7b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GithubAuthorizationService.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GithubAuthorizationService.java @@ -15,7 +15,7 @@ import com.predic8.membrane.annot.MCElement; -@MCElement(name="github", topLevel=false) +@MCElement(name="github", component =false) public class GithubAuthorizationService extends AuthorizationService { @Override public void init() { diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GoogleAuthorizationService.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GoogleAuthorizationService.java index 4aba9c7f76..b89bd121c2 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GoogleAuthorizationService.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/GoogleAuthorizationService.java @@ -15,7 +15,7 @@ import com.predic8.membrane.annot.MCElement; -@MCElement(name="google", topLevel=false) +@MCElement(name="google", component =false) public class GoogleAuthorizationService extends AuthorizationService { @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java index 1a1e865584..9aaab05118 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java @@ -172,7 +172,7 @@ public void setJwk(JwtSessionManager.Jwk jwk) { this.jwk = jwk; } - @MCElement(name="jwk", mixed = true, topLevel = false, id="bearerJwtToken-jwk") + @MCElement(name="jwk", mixed = true, component = false, id="bearerJwtToken-jwk") public static class Jwk extends Blob { } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/FlowInitiator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/FlowInitiator.java index ae172da40c..100a2e2bcb 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/FlowInitiator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/FlowInitiator.java @@ -29,7 +29,7 @@ import static com.predic8.membrane.core.interceptor.Outcome.*; import static java.nio.charset.StandardCharsets.UTF_8; -@MCElement(name = "flowInitiator") +@MCElement(name = "flowInitiator", excludeFromFlow = true) public class FlowInitiator extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(FlowInitiator.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/OAuth2PermissionCheckerInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/OAuth2PermissionCheckerInterceptor.java index b95744e7a8..fed1ee8ef6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/OAuth2PermissionCheckerInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/OAuth2PermissionCheckerInterceptor.java @@ -85,7 +85,7 @@ public static abstract class ValueSource { public abstract Object evaluate(Exchange exc); } - @MCElement(topLevel = false, name = "userInfo") + @MCElement(component = false, name = "userInfo") public static class UserInfoValueSource extends ValueSource { String field; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2server/Claim.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2server/Claim.java index 515cdc45c4..e31e9a0e65 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2server/Claim.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2server/Claim.java @@ -15,7 +15,7 @@ import com.predic8.membrane.annot.MCElement; -@MCElement(name="claim", topLevel=false, id="supportedClaims-claim") +@MCElement(name="claim", component =false, id="supportedClaims-claim") public class Claim { String claimName; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/OpenTelemetryInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/OpenTelemetryInterceptor.java index 0d93e52371..0759c7d81f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/OpenTelemetryInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/OpenTelemetryInterceptor.java @@ -263,7 +263,7 @@ public double getSampleRate() { return sampleRate; } - @MCElement(name = "customAttribute", topLevel = false) + @MCElement(name = "customAttribute", component = false) public static class CustomAttribute { private String name; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/exporter/OtlpExporter.java b/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/exporter/OtlpExporter.java index 91a1e3d3a3..26cf0d0549 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/exporter/OtlpExporter.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/opentelemetry/exporter/OtlpExporter.java @@ -33,7 +33,7 @@ /* * Otlp Implementation for the OpenTelemetry protocol */ -@MCElement(name = "otlpExporter", topLevel = false) +@MCElement(name = "otlpExporter", component = false) public class OtlpExporter implements OtelExporter { private static final int TIMEOUT_SECONDS = 30; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/RegistrationInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/RegistrationInterceptor.java index 7a33d23d3b..3193cad33c 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/registration/RegistrationInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/registration/RegistrationInterceptor.java @@ -32,7 +32,7 @@ /** * @description Allows account registration (!Experimental!) */ -@MCElement(name = "accountRegistration") +@MCElement(name = "accountRegistration", excludeFromFlow = true) public class RegistrationInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(RegistrationInterceptor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/rest/REST2SOAPInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/rest/REST2SOAPInterceptor.java index 0c2740e681..9c4c8445b6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/rest/REST2SOAPInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/rest/REST2SOAPInterceptor.java @@ -236,7 +236,7 @@ public String getShortDescription() { return "Transforms REST requests into SOAP and responses vice versa."; } - @MCElement(name = "mapping", topLevel = false, id = "rest2Soap-mapping") + @MCElement(name = "mapping", component = false, id = "rest2Soap-mapping") public static class Mapping extends AbstractXmlElement { public String regex; public String soapAction; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptor.java index b48a0780a6..c732689f6f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/rewrite/RewriteInterceptor.java @@ -39,7 +39,7 @@ *

* @topic 6. Misc */ -@MCElement(name = "rewriter", noEnvelope = true, topLevel = false) +@MCElement(name = "rewriter", noEnvelope = true, component = false) public class RewriteInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(RewriteInterceptor.class.getName()); @@ -50,7 +50,7 @@ public enum Type { REDIRECT_PERMANENT, } - @MCElement(name = "map", topLevel = false, id = "rewriter-map") + @MCElement(name = "map", component = false, id = "rewriter-map") public static class Mapping { public String to; public String from; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java index de3de379f2..1c4ee4218e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java @@ -14,15 +14,12 @@ package com.predic8.membrane.core.interceptor.schemavalidation.json; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLParser; -import com.github.fge.jsonschema.main.JsonSchemaFactory; import com.networknt.schema.*; import com.networknt.schema.Error; import com.networknt.schema.path.NodePath; -import com.networknt.schema.serialization.YamlMapperFactory; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.Interceptor.*; import com.predic8.membrane.core.interceptor.*; @@ -35,9 +32,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.concurrent.atomic.*; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/session/JwtSessionManager.java b/core/src/main/java/com/predic8/membrane/core/interceptor/session/JwtSessionManager.java index f36a699554..d6f9b2115c 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/session/JwtSessionManager.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/session/JwtSessionManager.java @@ -232,7 +232,7 @@ public void setJwk(Jwk jwk) { this.jwk = jwk; } - @MCElement(name = "jwk", mixed = true, topLevel = false, id = "jwtSessionManager-jwk") + @MCElement(name = "jwk", mixed = true, component = false, id = "jwtSessionManager-jwk") public static class Jwk extends Blob { } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/tunnel/TCPInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/tunnel/TCPInterceptor.java index 8d15840da0..197ec3b152 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/tunnel/TCPInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/tunnel/TCPInterceptor.java @@ -25,7 +25,7 @@ * and not inspected. * @default false */ -@MCElement(name = "tcp") +@MCElement(name = "tcp", excludeFromFlow = true) public class TCPInterceptor extends AbstractInterceptor { public TCPInterceptor() { diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/ws_addressing/WsaEndpointRewriterInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ws_addressing/WsaEndpointRewriterInterceptor.java index 3e887c8bd9..1b6dddd38d 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/ws_addressing/WsaEndpointRewriterInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/ws_addressing/WsaEndpointRewriterInterceptor.java @@ -27,7 +27,7 @@ import static com.predic8.membrane.core.interceptor.Outcome.ABORT; import static com.predic8.membrane.core.interceptor.Outcome.*; -@MCElement(name="wsaEndpointRewriter") +@MCElement(name="wsaEndpointRewriter", excludeFromFlow = true) public class WsaEndpointRewriterInterceptor extends AbstractInterceptor { private static final Logger log = LoggerFactory.getLogger(WsaEndpointRewriterInterceptor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/kubernetes/Bean.java b/core/src/main/java/com/predic8/membrane/core/kubernetes/Bean.java deleted file mode 100644 index c815380174..0000000000 --- a/core/src/main/java/com/predic8/membrane/core/kubernetes/Bean.java +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright 2022 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.core.kubernetes; - -import com.predic8.membrane.annot.MCChildElement; -import com.predic8.membrane.annot.MCElement; - -/** - * @description - * "bean" should be used for Kubernetes only. Experimental. - */ -@MCElement(name = "bean") -public class Bean { - - Object bean; - - public Object getBean() { - return bean; - } - - @MCChildElement - public void setBean(Object bean) { - this.bean = bean; - } -} diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java index fe74f99f56..9d12344f8e 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java @@ -44,7 +44,7 @@ * @description The api proxy extends the serviceProxy with API related functions like OpenAPI support and path parameters. * @topic 1. Proxies and Flow */ -@MCElement(name = "api") +@MCElement(name = "api", topLevel = true) public class APIProxy extends ServiceProxy implements Polyglot, XMLSupport { private static final Logger log = LoggerFactory.getLogger(APIProxy.class.getName()); @@ -258,7 +258,7 @@ public void setLanguage(Language language) { this.language = language; } - @MCElement(name = "description", topLevel = false, mixed = true) + @MCElement(name = "description", component = false, mixed = true) public static class ApiDescription { private String content; diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPIInterceptor.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPIInterceptor.java index 348584cc05..9ed2b64c78 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPIInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPIInterceptor.java @@ -22,7 +22,6 @@ import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.openapi.*; import com.predic8.membrane.core.openapi.validators.*; -import com.predic8.membrane.core.proxies.Proxy; import com.predic8.membrane.core.proxies.*; import com.predic8.membrane.core.util.ConfigurationException; import io.swagger.v3.oas.models.*; diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPISpec.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPISpec.java index f270bb68a1..0a287cabee 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPISpec.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPISpec.java @@ -16,7 +16,6 @@ package com.predic8.membrane.core.openapi.serviceproxy; -import com.fasterxml.jackson.annotation.*; import com.predic8.membrane.annot.*; import static com.predic8.membrane.core.openapi.serviceproxy.OpenAPISpec.YesNoOpenAPIOption.ASINOPENAPI; @@ -24,7 +23,7 @@ /** * @description Reads an OpenAPI description and deploys an API with the information of it. */ -@MCElement(name = "openapi", topLevel = false) +@MCElement(name = "openapi", component = false) public class OpenAPISpec implements Cloneable { public String location; diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/Rewrite.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/Rewrite.java index 6f8957d69c..cb1032492a 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/Rewrite.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/Rewrite.java @@ -34,7 +34,7 @@ /** * @description */ -@MCElement(name = "rewrite", topLevel = false) +@MCElement(name = "rewrite", component = false) public class Rewrite implements Cloneable { private static final Logger log = LoggerFactory.getLogger(Rewrite.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java index d5d027dca6..44ffb320e9 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java @@ -96,7 +96,7 @@ public void setPath(Path path) { * Supports dynamic destinations through expressions. *

*/ - @MCElement(name = "target", topLevel = false) + @MCElement(name = "target", component = false) public static class Target implements XMLSupport { private String host; private int port = -1; diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SSLProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SSLProxy.java index b92e7347ac..69f0c21905 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SSLProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SSLProxy.java @@ -57,7 +57,7 @@ public class SSLProxy implements Proxy { private boolean useAsDefault = true; private List sslInterceptors = new ArrayList<>(); - @MCElement(id = "sslProxy-target", name="target", topLevel = false) + @MCElement(id = "sslProxy-target", name="target", component = false) public static class Target { private int port = -1; private String host; diff --git a/core/src/main/java/com/predic8/membrane/core/sslinterceptor/GateKeeperClientInterceptor.java b/core/src/main/java/com/predic8/membrane/core/sslinterceptor/GateKeeperClientInterceptor.java index 44563f58cc..1fb3aeadd7 100644 --- a/core/src/main/java/com/predic8/membrane/core/sslinterceptor/GateKeeperClientInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/sslinterceptor/GateKeeperClientInterceptor.java @@ -18,8 +18,6 @@ import com.google.common.collect.*; import com.predic8.membrane.annot.*; import com.predic8.membrane.core.*; -import com.predic8.membrane.core.exchange.*; -import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.transport.http.*; import com.predic8.membrane.core.transport.http.client.*; @@ -34,7 +32,7 @@ /** * Connects to the predic8 Gatekeeper to check, whether access is allowed or not. */ -@MCElement(id = "sslProxy-gatekeeper", name = "gatekeeper", topLevel = false) +@MCElement(id = "sslProxy-gatekeeper", name = "gatekeeper", component = false) public class GateKeeperClientInterceptor implements SSLInterceptor { protected final String name; diff --git a/core/src/main/java/com/predic8/membrane/core/sslinterceptor/RouterIpResolverInterceptor.java b/core/src/main/java/com/predic8/membrane/core/sslinterceptor/RouterIpResolverInterceptor.java index cff7e51d5d..473a9e1cd8 100644 --- a/core/src/main/java/com/predic8/membrane/core/sslinterceptor/RouterIpResolverInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/sslinterceptor/RouterIpResolverInterceptor.java @@ -46,7 +46,7 @@ *

* This interceptor is helpful in scenarios with multiple redundant routers for inbound HTTP requests. */ -@MCElement(id = "sslProxy-routerIpResolver", name = "routerIpResolver", topLevel = false) +@MCElement(id = "sslProxy-routerIpResolver", name = "routerIpResolver", component = false) public class RouterIpResolverInterceptor implements SSLInterceptor { private final Logger log = LoggerFactory.getLogger(RouterIpResolverInterceptor.class); diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/client/AuthenticationConfiguration.java b/core/src/main/java/com/predic8/membrane/core/transport/http/client/AuthenticationConfiguration.java index 04bf586fd4..bc0318606d 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/client/AuthenticationConfiguration.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/client/AuthenticationConfiguration.java @@ -32,7 +32,7 @@ * * @topic 4. Transports and Clients */ -@MCElement(name="authentication", topLevel=false) +@MCElement(name="authentication", component =false) public class AuthenticationConfiguration { private String username; diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/client/ConnectionConfiguration.java b/core/src/main/java/com/predic8/membrane/core/transport/http/client/ConnectionConfiguration.java index 6634de41ba..478aa4d3cc 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/client/ConnectionConfiguration.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/client/ConnectionConfiguration.java @@ -35,7 +35,7 @@ * * @topic 4. Transports and Clients */ -@MCElement(name="connection", topLevel=false) +@MCElement(name="connection", component =false) public class ConnectionConfiguration { private long keepAliveTimeout = 4000; diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/client/ProxyConfiguration.java b/core/src/main/java/com/predic8/membrane/core/transport/http/client/ProxyConfiguration.java index 7c0e05a2d7..51693fc2d1 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/client/ProxyConfiguration.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/client/ProxyConfiguration.java @@ -45,7 +45,7 @@ * * @topic 4. Transports and Clients */ -@MCElement(name="proxy", topLevel=false, id="proxy-configuration") +@MCElement(name="proxy", component =false, id="proxy-configuration") public class ProxyConfiguration { private String host; diff --git a/core/src/main/java/com/predic8/membrane/core/util/RedisConnector.java b/core/src/main/java/com/predic8/membrane/core/util/RedisConnector.java index bd50168f97..c1491b7d97 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/RedisConnector.java +++ b/core/src/main/java/com/predic8/membrane/core/util/RedisConnector.java @@ -24,7 +24,7 @@ import java.util.HashSet; import java.util.Set; -@MCElement(name = "redis", topLevel = true) +@MCElement(name = "redis", component = true) public class RedisConnector implements InitializingBean { private JedisPool pool; private JedisSentinelPool sentinelPool; diff --git a/distribution/examples/extending-membrane/custom-interceptor/README.md b/distribution/examples/extending-membrane/custom-interceptor/README.md index cbfea762c1..12f23dbeac 100644 --- a/distribution/examples/extending-membrane/custom-interceptor/README.md +++ b/distribution/examples/extending-membrane/custom-interceptor/README.md @@ -28,20 +28,24 @@ To run the example execute the following steps: Using maven, we create a jar file and copy the compiled jar file into the libs directory of membrane to make the new interceptor available to the router. -In the proxies.xml file, we define a name for our interceptor and write its fully qualified name, so we can use our interceptor on . You can see it in the line below. +In the apis.yaml file, we define a name for our interceptor and write its fully qualified name, so we can use our interceptor in the flow: -``` - +```yaml +components: + myInterceptor: + bean: + class: com.predic8.MyInterceptor ``` -Again in the proxies.xml file inside `` tag you can see that we added the interceptor using the beanname we defined above. +Again in the apis.yaml file inside `api` you can see that we added the interceptor using the beanname we defined above. -``` - - - - +```yaml +api: + port: 2000 + flow: + - $ref: "#/components/myInterceptor" + - return: {} ``` When we run the membrane using membrane.sh you can see that in the console that requests and responses are being intercepted by our custom interceptor. diff --git a/distribution/examples/extending-membrane/custom-interceptor/apis.yaml b/distribution/examples/extending-membrane/custom-interceptor/apis.yaml new file mode 100644 index 0000000000..f2a48f844e --- /dev/null +++ b/distribution/examples/extending-membrane/custom-interceptor/apis.yaml @@ -0,0 +1,12 @@ +components: + myInterceptor: + bean: + class: com.predic8.MyInterceptor + +--- + +api: + port: 2000 + flow: + - $ref: "#/components/myInterceptor" + - return: {} \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/OfflineExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/OfflineExampleTest.java index 2f81d3ff3f..d6edbf6923 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/OfflineExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/OfflineExampleTest.java @@ -1,3 +1,17 @@ +/* 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.examples.withoutinternet; import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; diff --git a/docs/JAVADOC.md b/docs/JAVADOC.md index 636cd029bd..9df73c71a8 100644 --- a/docs/JAVADOC.md +++ b/docs/JAVADOC.md @@ -82,9 +82,9 @@ Therefore, while most of the project historically used XML, everything can be ex `@MCElement` can only be used on classes. If you write `` or `foo:` in the configuration, this will cause the Java Class to be instantiated at startup. - If `@MCElement(name="foo",topLevel=true)` has `topLevel==true` (which it has by default), ``/`foo:` can be used as the top-level element in the configuration. The Foo instance will be created as a Spring Bean (in the XML case) and take part of its life cycle. The most basic example is using ``/`router:` at the top level: This will create and start a `Router` instance which starts Membrane API Gateway. + If `@MCElement(name="foo",component=true)` has `component==true` (which it has by default), ``/`foo:` can be used as a component element in the configuration. The Foo instance will be created as a Spring Bean (in the XML case) and take part of its life cycle. The most basic example is using ``/`router:` at the top level: This will create and start a `Router` instance which starts Membrane API Gateway. - Top-Level elements can carry a unique `id` attribute. This will register the instance Spring Bean with this ID. + Component elements can carry a unique `id` attribute. This will register the instance Spring Bean with this ID. * **`@MCAttribute`** Can be used on Setter methods. The Java Type of the Setter's parameter must be a simple type, enum, String or a `@MCElement` annotated class.