diff --git a/annot/pom.xml b/annot/pom.xml index 2bc5c5082b..d3b473853c 100644 --- a/annot/pom.xml +++ b/annot/pom.xml @@ -61,6 +61,11 @@ spotbugs-annotations 4.9.8 + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java new file mode 100644 index 0000000000..ebc39e8d2d --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java @@ -0,0 +1,23 @@ +package com.predic8.membrane.annot.beanregistry; + +import java.lang.reflect.Method; + +/** + * A BeanLifecycleManager supports the init-destroy lifecycle of beans. + * + * Beans are inited (@PostConstruct method called) when they are being defined, that is before their instance is + * published via the registry. + * + * Beans are destroyed (@PreDestroy method called) when close() is called on the registry. + * + * The registry implements this interface. + */ +public interface BeanLifecycleManager { + /** + * Tells the registry that the method should be called on the bean when the registry is + * closed. + * + * The registry should call all pre-destroy-callbacks in reverse order in which they were registered. + */ + void addPreDestroyCallback(Object bean, Method method); +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java index d562b2ea33..a417d50be8 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java @@ -15,6 +15,7 @@ import com.predic8.membrane.annot.*; +import java.lang.reflect.Method; import java.util.*; import java.util.function.*; @@ -56,4 +57,9 @@ public interface BeanRegistry { * @return the existing or newly created and registered bean instance */ T registerIfAbsent(Class type, Supplier supplier); + + /** + * Release all resources. + */ + void close(); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java index 8ea53231a7..5aab389bdf 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java @@ -20,6 +20,7 @@ import org.slf4j.*; import javax.annotation.concurrent.*; +import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.*; import java.util.function.*; @@ -36,7 +37,7 @@ * For K8S UUID and name is needed cause name is only unique within a namespace. * */ -public class BeanRegistryImplementation implements BeanRegistry, BeanCollector { +public class BeanRegistryImplementation implements BeanRegistry, BeanCollector, BeanLifecycleManager { private static final Logger log = LoggerFactory.getLogger(BeanRegistryImplementation.class); @@ -57,9 +58,15 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector { */ private final Object uniqueClassInitialization = new Object(); + @GuardedBy("preDestroyCallbacks") + private final List preDestroyCallbacks = new ArrayList<>(); + record UidAction(String uid, WatchAction action) { } + record PreDestroyCallback(Object bean, Method method) { + } + public BeanRegistryImplementation(BeanCacheObserver observer, BeanRegistryAware registryAware, Grammar grammar) { this.observer = observer; this.grammar = grammar; @@ -198,4 +205,28 @@ public T registerIfAbsent(Class type, Supplier supplier) { return beanName != null ? beanName : "#" + uuid; } + /** + * Registers a @PreDestroy callback for the given bean. + */ + public void addPreDestroyCallback(Object bean, Method method) { + synchronized (preDestroyCallbacks) { + preDestroyCallbacks.add(new PreDestroyCallback(bean, method)); + } + } + + public void close() { + List callbacks; + synchronized (preDestroyCallbacks) { + callbacks = new ArrayList<>(preDestroyCallbacks); + preDestroyCallbacks.clear(); + } + callbacks.reversed().forEach(pc -> { + try { + pc.method.invoke(pc.bean); + } catch (Exception e) { + log.error("Could not invoke preDestroy method of {}: {}", pc.bean, e.getMessage()); + } + }); + } + } diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java new file mode 100644 index 0000000000..98cb0d2f95 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java @@ -0,0 +1,68 @@ +package com.predic8.membrane.annot.beanregistry; + +import com.predic8.membrane.annot.Grammar; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractRefreshableApplicationContext; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Adapter between Membrane's BeanRegistry and Spring's ApplicationContext. + * + * Methods are only implemented on a need-to-use basis. + */ +public class SpringContextAdapter implements BeanRegistry { + + private final AbstractRefreshableApplicationContext ac; + + public SpringContextAdapter(AbstractRefreshableApplicationContext ac) { + this.ac = ac; + } + + @Override + public Object resolve(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public List getBeans() { + return List.of(ac.getBeanDefinitionNames()).stream().map(ac::getBean).toList(); + } + + @Override + public Grammar getGrammar() { + throw new UnsupportedOperationException(); + } + + @Override + public List getBeans(Class clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBean(Class clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public void register(String beanName, Object bean) { + throw new UnsupportedOperationException(); + } + + @Override + public T registerIfAbsent(Class type, Supplier supplier) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + ac.close(); + } + + public ApplicationContext getApplicationContext() { + return ac; + } +} 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 48799b006e..d1c59f87d9 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 @@ -19,9 +19,12 @@ import com.networknt.schema.*; import com.networknt.schema.Error; import com.predic8.membrane.annot.*; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import com.predic8.membrane.annot.beanregistry.*; import org.jetbrains.annotations.*; import org.slf4j.*; +import org.springframework.util.ReflectionUtils; import java.io.*; import java.lang.reflect.*; @@ -32,6 +35,7 @@ import static com.predic8.membrane.annot.yaml.MethodSetter.*; import static com.predic8.membrane.annot.yaml.NodeValidationUtils.*; import static java.nio.charset.StandardCharsets.*; +import static java.util.List.of; import static java.util.UUID.*; public class GenericYamlParser { @@ -136,12 +140,12 @@ private static void validate(Grammar grammar, JsonNode input) throws YamlSchemaV *

Ensures the node contains exactly one key (the kind), resolves the Java class via the * grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.

*/ - public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, BeanRegistry registry) throws ParsingException { + public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, R registry) throws ParsingException { ensureSingleKey(node); Class clazz = grammar.getElement(kind); if (clazz == null) throw new ParsingException("Did not find java class for kind '%s'.".formatted(kind), node); - return createAndPopulateNode(new ParsingContext(kind, registry, grammar), clazz, node.get(kind)); + return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar), clazz, node.get(kind)); } /** @@ -151,7 +155,7 @@ public static Object readMembraneObject(String kind, Grammar grammar, JsonNode n * values are produced by {@link MethodSetter#getMethodSetter(ParsingContext, Class, String)}. A top-level {@code "$ref"} injects a previously defined bean. * All failures are wrapped in a {@link ParsingException} with location information. */ - public static T createAndPopulateNode(ParsingContext ctx, Class clazz, JsonNode node) throws ParsingException { + public static T createAndPopulateNode(ParsingContext ctx, Class clazz, JsonNode node) throws ParsingException { try { T configObj = clazz.getConstructor().newInstance(); if (node.isArray()) { @@ -186,9 +190,8 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js } if (!required.isEmpty()) throw new ParsingException("Missing required fields: " + required.stream().map(McYamlIntrospector::getSetterName).toList(), node); - return configObj; - } - catch (NoClassDefFoundError e) { + return handlePostConstructAndPreDestroy(ctx, configObj); + } catch (NoClassDefFoundError e) { if (e.getCause() != null) { var missingClass = e.getCause().getMessage(); // TODO: Better use ExceptionUtil.getRootCause() but it isn't visible in annot. var msg = "Could not create bean with class: %s\nMissing class: %s\n".formatted(clazz, missingClass); @@ -196,15 +199,14 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js throw new ParsingException(msg, node); // TODO: Cause we know the reason, shorten output. } throw new ParsingException(e, node); - } - catch (Throwable cause) { + } catch (Throwable cause) { throw new ParsingException(cause, node); } } private static List extractComponentBeanDefinitions(JsonNode componentsNode) { if (componentsNode == null || componentsNode.isNull()) - return List.of(); + return of(); if (!componentsNode.isObject()) throw new ParsingException("Expected object for 'components'.", componentsNode); @@ -240,7 +242,7 @@ private static List extractComponentBeanDefinitions(JsonNode com * 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 { + 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()); @@ -262,7 +264,7 @@ private static void applyObjectLevelRef(ParsingContext ctx, Class parentC } } - private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { + private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { try { return ctx.registry().resolve(refNode.asText()); } catch (RuntimeException e) { @@ -270,12 +272,12 @@ private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { } } - public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { ensureArray(node); return parseListExcludingStartEvent(context, node); } - private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + 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))); @@ -287,15 +289,39 @@ public static List parseListIncludingStartEvent(ParsingContext context, * Parses a single-item map node like { kind: {...} } by extracting the only key and * delegating to {@link #parseMapToObj(ParsingContext, JsonNode, String)}. */ - private static Object parseMapToObj(ParsingContext context, JsonNode node) throws ParsingException { + private static Object parseMapToObj(ParsingContext context, JsonNode node) throws ParsingException { ensureSingleKey(node); String key = node.fieldNames().next(); return parseMapToObj(context, node.get(key), key); } - private static Object parseMapToObj(ParsingContext ctx, JsonNode node, String key) throws ParsingException { + private static Object parseMapToObj(ParsingContext ctx, JsonNode node, String key) throws ParsingException { if ("$ref".equals(key)) return ctx.registry().resolve(node.asText()); return createAndPopulateNode(ctx.updateContext(key), ctx.resolveClass(key), node); } + + /** + * Calls the @PostConstruct method on the bean and returns it. If there are @PreDestroy methods, they will be + * registered within the registry. + */ + private static T handlePostConstructAndPreDestroy(ParsingContext ctx, T bean) { + ReflectionUtils.doWithMethods(bean.getClass(), method -> { + if (method.isAnnotationPresent(PostConstruct.class)) { + try { + method.setAccessible(true); + method.invoke(bean); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getTargetException()); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new RuntimeException(e); + } + } + if (method.isAnnotationPresent(PreDestroy.class)) { + method.setAccessible(true); + ctx.registry().addPreDestroyCallback(bean, method); + } + }); + return bean; + } } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java index 9dd2636173..a3a80e2f88 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java @@ -15,7 +15,9 @@ package com.predic8.membrane.annot.yaml; import com.predic8.membrane.annot.*; +import com.predic8.membrane.annot.beanregistry.BeanLifecycleManager; import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation; /** * Immutable parsing state passed down while traversing YAML. @@ -23,7 +25,7 @@ * - registry: access to already materialized beans (e.g., for $ref/reference attributes). * - grammar: resolves element names to Java classes via local/global lookups. */ -public record ParsingContext(String context, BeanRegistry registry, Grammar grammar) { +public record ParsingContext(String context, T registry, Grammar grammar) { ParsingContext updateContext(String context) { return new ParsingContext(context, registry, grammar); 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 77325e596b..ba3e92d850 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java @@ -14,11 +14,19 @@ package com.predic8.membrane.annot; +import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import com.predic8.membrane.annot.util.CompilerHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; + +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 com.predic8.membrane.annot.util.StructureAssertionUtil.clazz; public class ParsingTest { @@ -90,4 +98,203 @@ public class Child2 extends AbstractDemoChildElement { """)); } + + @Test + public void afterPropertiesSetAndDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import org.springframework.beans.factory.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement implements InitializingBean, DisposableBean { + int value = 0; + public int getValue() { return value; } + + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + """)); + + assertStructure( + br, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + @Test + public void afterPropertiesSetAndDestroyAndPostConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import org.springframework.beans.factory.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement implements InitializingBean, DisposableBean { + int value = 0; + public int getValue() { return value; } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + + + """)); + + assertStructure( + br, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + @Test + public void postConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement { + int value = 0; + public int getValue() { + return value; + } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + + + """)); + + assertStructure( + br, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + } 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 ac3ae8befe..52e9b5cfc6 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java @@ -14,12 +14,16 @@ package com.predic8.membrane.annot; +import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import com.predic8.membrane.annot.util.CompilerHelper; import com.predic8.membrane.annot.yaml.YamlSchemaValidationException; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; import java.lang.reflect.InvocationTargetException; +import java.util.List; import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO; import static com.predic8.membrane.annot.util.CompilerHelper.*; @@ -601,4 +605,69 @@ private Throwable getCause(Throwable e) { return e; } + @Test + public void postConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root", topLevel=true, component=false) + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement { + int value = 0; + public int getValue() { + return value; + } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseYAML(result, """ + root: + child: {} + """); + + assertStructure( + br, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + br.close(); + + assertStructure( + beans, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + } 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 224b8ffb2f..6c4b9f0bc5 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 @@ -13,15 +13,21 @@ limitations under the License. */ package com.predic8.membrane.annot.util; +import com.predic8.membrane.annot.Grammar; import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import org.hamcrest.*; import org.hamcrest.collection.*; import org.jetbrains.annotations.*; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; +import org.springframework.context.support.AbstractRefreshableApplicationContext; import javax.tools.*; import java.io.*; import java.lang.reflect.*; import java.util.*; +import java.util.function.Supplier; import java.util.regex.*; import java.util.regex.Matcher; import java.util.stream.*; @@ -82,17 +88,12 @@ public static BeanRegistry parseYAML(CompilerResult cr, String yamlConfig) { }); } - public static void parseXML(CompilerResult cr, String xmlSpringConfig) { + public static BeanRegistry parseXML(CompilerResult cr, String xmlSpringConfig) { CompositeClassLoader cl = xmlClassLoader(cr, xmlSpringConfig); - withContextClassLoader(cl, () -> { + return 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; + return new SpringContextAdapter((AbstractRefreshableApplicationContext) context); }); } diff --git a/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java b/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java index 2f6891835e..1b2c9d055a 100644 --- a/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java +++ b/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java @@ -14,6 +14,7 @@ package com.predic8.membrane.core.config.spring.k8s; +import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation; import com.predic8.membrane.annot.yaml.GenericYamlParser; import com.predic8.membrane.core.config.spring.GrammarAutoGenerated; import com.predic8.membrane.core.interceptor.Interceptor; @@ -209,7 +210,7 @@ void missingKindDefaultsToApi() { assertEquals(1001, ((APIProxy) e.getSpec()).getPort()); } - private static List parseEnvelopes(String yaml, BeanRegistry registry) { + private static List parseEnvelopes(String yaml, BeanRegistryImplementation registry) { GrammarAutoGenerated generator = new GrammarAutoGenerated(); try { return new GenericYamlParser(generator, yaml) diff --git a/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java b/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java index 26e03653fc..f18651f893 100644 --- a/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java +++ b/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java @@ -38,6 +38,7 @@ import org.junit.jupiter.params.provider.*; import java.io.*; +import java.lang.reflect.Method; import java.util.*; import java.util.function.*; import java.util.stream.*; @@ -71,15 +72,15 @@ Stream successCases() { setHost("localhost"); setPort(3000); }}; - BeanRegistry memReg = new TestRegistry().with("mem", mem); + TestBeanRegistry memReg = new TestBeanRegistry().with("mem", mem); Target target = new Target() {{ setUrl("https://ref.example"); }}; - BeanRegistry targetReg = new TestRegistry().with("target", target); + TestBeanRegistry targetReg = new TestBeanRegistry().with("target", target); ResponseInterceptor responseInterceptor = new ResponseInterceptor(); - BeanRegistry responseReg = new TestRegistry().with("response", responseInterceptor); + TestBeanRegistry responseReg = new TestBeanRegistry().with("response", responseInterceptor); return Stream.of( ok( @@ -292,7 +293,7 @@ Stream successCases() { } Stream errorCases() { - BeanRegistry empty = new TestRegistry(); + TestBeanRegistry empty = new TestBeanRegistry(); return Stream.of( err( "invalid_ref", @@ -311,28 +312,28 @@ Stream errorCases() { ); } - record Case(String testName, String yaml, BeanRegistry reg, java.util.function.Consumer check) { + record Case(String testName, String yaml, TestBeanRegistry reg, java.util.function.Consumer check) { @Override public @NotNull String toString() { return testName; } } - record ErrCase(String testName, String yaml, BeanRegistry reg, Class expected) { + record ErrCase(String testName, String yaml, TestBeanRegistry reg, Class expected) { @Override public @NotNull String toString() { return testName; } } private static Case ok(String testName, String yaml, java.util.function.Consumer check) { return new Case(testName, yaml, null, check); } - private static Case ok(String testName, String yaml, BeanRegistry reg, java.util.function.Consumer check) { + private static Case ok(String testName, String yaml, TestBeanRegistry reg, java.util.function.Consumer check) { return new Case(testName, yaml, reg, check); } - private static ErrCase err(String testName, String yaml, BeanRegistry reg, Class expected) { + private static ErrCase err(String testName, String yaml, TestBeanRegistry reg, Class expected) { return new ErrCase(testName, yaml, reg, expected); } - static class TestRegistry implements BeanRegistry, BeanCollector { + static class TestBeanRegistry implements BeanRegistry, BeanCollector, BeanLifecycleManager { private final Map refs = new HashMap<>(); - TestRegistry with(String key, Object v) { refs.put(key, v); return this; } + TestBeanRegistry with(String key, Object v) { refs.put(key, v); return this; } @Override public Object resolve(String ref) { return refs.get(ref); } @Override @@ -373,10 +374,16 @@ public void register(String beanName, Object object) {} public T registerIfAbsent(Class type, Supplier supplier) { return type.cast(null); } + + @Override + public void close() {} + + @Override + public void addPreDestroyCallback(Object bean, Method method) {} } - private static APIProxy parse(String yaml, BeanRegistry reg) { - return GenericYamlParser.createAndPopulateNode(new ParsingContext("api", reg, K8S_HELPER), APIProxy.class, parse(yaml)); + private static APIProxy parse(String yaml, TestBeanRegistry reg) { + return GenericYamlParser.createAndPopulateNode(new ParsingContext("api", reg, K8S_HELPER), APIProxy.class, parse(yaml)); } public static JsonNode parse(String yaml) {