diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanContainer.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanContainer.java index b022a63950..d3eba278b7 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanContainer.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanContainer.java @@ -17,6 +17,7 @@ import com.predic8.membrane.annot.Grammar; import com.predic8.membrane.annot.bean.BeanFactory; import com.predic8.membrane.annot.yaml.GenericYamlParser; +import org.jetbrains.annotations.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +72,7 @@ public String toString() { return "BeanContainer: %s of %s singleton: %s".formatted( definition.getName(),definition.getKind(),singleton.get()); } - private synchronized Object define(BeanRegistryImplementation registry, Grammar grammar) { + private synchronized @NotNull Object define(BeanRegistryImplementation registry, Grammar grammar) { log.debug("defining bean: {}", definition.getNode()); try { if ("bean".equals(definition.getKind())) { @@ -86,7 +87,7 @@ private synchronized Object define(BeanRegistryImplementation registry, Grammar } } - public Object getOrCreate(BeanRegistryImplementation registry, Grammar grammar) { + public @NotNull Object getOrCreate(BeanRegistryImplementation registry, Grammar grammar) { boolean prototype = isPrototypeScope(getDefinition()); // Prototypes are created anew every time. 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 a417d50be8..9e16951f38 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 @@ -39,6 +39,15 @@ public interface BeanRegistry { */ Optional getBean(Class clazz); + /** + * Retrieves a bean with the specified name. + * @param name the name of the bean + * @param clazz the class of the bean + * @return Optional containing the bean + * @param the bean type + */ + Optional getBean(String name, Class clazz); + /** * Registers a bean with the specified name. * 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 5aab389bdf..52f9a7fa2b 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 @@ -172,6 +172,12 @@ public Optional getBean(Class clazz) { return beans.size() == 1 ? Optional.of(beans.getFirst()) : Optional.empty(); } + public Optional getBean(String beanname, Class clazz) { + return getFirstByName(beanname) + .map(bc -> bc.getOrCreate(this, grammar)) + .map(clazz::cast); + } + public void register(String beanName, Object bean) { if (bean == null) throw new IllegalArgumentException("bean must not be null"); 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 index 98cb0d2f95..f58642cfb0 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java @@ -47,6 +47,11 @@ public Optional getBean(Class clazz) { throw new UnsupportedOperationException(); } + @Override + public Optional getBean(String name, Class clazz) { + throw new UnsupportedOperationException(); + } + @Override public void register(String beanName, Object bean) { throw new UnsupportedOperationException(); 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 d392b4cf2b..76d9dda197 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 @@ -19,6 +19,7 @@ import java.lang.reflect.*; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.*; import static java.lang.Character.*; @@ -145,6 +146,18 @@ public static String getSetterName(Method setter) { return toLowerCase(property.charAt(0)) + property.substring(1); } + public static boolean hasOtherAttributes(Class clazz) { + return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCOtherAttributes.class)).count() > 0; + } + + public static boolean hasAttributes(Class clazz) { + return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCAttribute.class)).count() > 0; + } + + public static boolean hasChildren(Class clazz) { + return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCChildElement.class)).count() > 0; + } + public static Method getAnySetter(Class clazz) { return stream(clazz.getMethods()) .filter(McYamlIntrospector::isSetter) 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 9ba28ff4ee..d60a75fab1 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 @@ -55,6 +55,12 @@ public MethodSetter(Method setter, Class beanClass) { } Class beanClass = null; if (setter == null) { + // if the element ONLY has a MCOtherAttributes and no MCAttributes and no MCChildElement setters, we avoid + // global keyword resolution: the keyword will always be a key for MCOtherAttributes + if (hasOtherAttributes(clazz) && !hasAttributes(clazz) && !hasChildren(clazz)) { + return new MethodSetter(getAnySetter(clazz), null); + } + try { beanClass = ctx.grammar().getLocal(ctx.context(), key); if (beanClass == null) diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java index e01dd43e25..87688eafd8 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLComponentsParsingTest.java @@ -339,6 +339,35 @@ public void componentRefersToAnotherComponent() { ); } + @Test + public void componentIdIsWordFromGrammar() { + assertStructure( + parse(""" + components: + bearerToken: + bearerToken: + header: Authorization + --- + api: + flow: + - oauth2authserver: + issuer: https://issuer + otherFields: abc + $ref: "#/components/bearerToken" + """), + 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(""" diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/chain/ChainInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/chain/ChainInterceptor.java index 07d2c6ebf5..2f3ccce02f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/chain/ChainInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/chain/ChainInterceptor.java @@ -14,20 +14,17 @@ package com.predic8.membrane.core.interceptor.chain; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.annot.MCElement; -import com.predic8.membrane.annot.Required; -import com.predic8.membrane.core.exchange.Exchange; -import com.predic8.membrane.core.interceptor.Interceptor; -import com.predic8.membrane.core.interceptor.Outcome; -import com.predic8.membrane.core.interceptor.flow.AbstractFlowWithChildrenInterceptor; -import com.predic8.membrane.core.util.ConfigurationException; - -import java.util.List; -import java.util.Optional; +import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.flow.*; +import com.predic8.membrane.core.util.*; +import org.springframework.beans.factory.*; + +import java.util.*; /** - * @description A Chain groups multiple interceptors into reusable components, reducing redundancy in API configurations. + * @description A Chain groups multiple interceptors into reusable components, reducing redundancy in API configurations. */ @MCElement(name = "chain") public class ChainInterceptor extends AbstractFlowWithChildrenInterceptor { @@ -42,9 +39,28 @@ public void init() { } private List getInterceptorChainForRef(String ref) { - return Optional.of(router.getBeanFactory().getBean(ref, ChainDef.class)) - .map(ChainDef::getFlow) - .orElseThrow(() -> new ConfigurationException("No chain found for reference: " + ref)); + return getBean(ref, ChainDef.class) + .orElseThrow(() -> new ConfigurationException("No chain found for reference: " + ref)) + .getFlow(); + } + + /** + * TODO: Temporary fix till we have a central configuration independant lookup + */ + private Optional getBean(String name, Class clazz) { + if (router.getRegistry() != null) { + var bean = router.getRegistry().getBean(name, clazz); + if (bean.isPresent()) + return bean; + } + // From XML + if (router.getBeanFactory() != null) { + try { + return Optional.of(router.getBeanFactory().getBean(name, clazz)); + } catch (NoSuchBeanDefinitionException ignored) { + } + } + return Optional.empty(); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java b/core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java index a3136d61a5..c5ed72bc68 100644 --- a/core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java +++ b/core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java @@ -405,7 +405,7 @@ public void setRegistry(BeanRegistry registry) { @Override public BeanRegistry getRegistry() { - return mainComponents.getRegistry(); + return mainComponents.getRegistry(); } public void applyConfiguration(Configuration configuration) { 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 f18651f893..500bfaec48 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 @@ -367,6 +367,11 @@ public Optional getBean(Class clazz) { return Optional.empty(); } + @Override + public Optional getBean(String beanName, Class clazz) { + return Optional.empty(); + } + @Override public void register(String beanName, Object object) {} diff --git a/distribution/examples/extending-membrane/reusable-plugin-chains/README.md b/distribution/examples/extending-membrane/reusable-plugin-chains/README.md index 77829fc7d6..b7ac7e1b93 100644 --- a/distribution/examples/extending-membrane/reusable-plugin-chains/README.md +++ b/distribution/examples/extending-membrane/reusable-plugin-chains/README.md @@ -1,20 +1,23 @@ # Reusable Plugin Chains -This example demonstrates how using a shared chain helps standardize both request and response handling while letting each API define its own behavior. Chains group plugins and interceptors into reusable components, significantly reducing redundancy and the overall size of your proxies.xml configuration, especially when managing multiple APIs. +This example demonstrates how shared chains helps standardize both request and response handling while letting each API define its own behavior. A chain groups plugins and interceptors into reusable components, significantly reducing redundancy and the overall size of your configuration, especially when managing multiple APIs. + +## **Running the Example** -### **Running the Example** 1. **Start the Router** ```sh ./router-service.sh # Linux/Mac router-service.bat # Windows ``` 2. **Test the APIs:** - - **API 1 (Port 2000) → Returns `200 OK`** + - **API 1:** ```sh - curl -i http://localhost:2000 + curl -i http://localhost:2000/foo ``` - - **API 2 (Port 2001) → Returns `404 Not Found`** + Observe the gateway log output for 'Path: ...' + - **API 2:** ```sh - curl -i http://localhost:2001 - ``` -3. **Check `proxies.xml`** to see how chains are applied. + curl -i http://localhost:2000/bar + ``` + Observe the gateway output and the response HTTP headers +3. **Check `apis.yaml`** to see how chains are applied. diff --git a/distribution/examples/extending-membrane/reusable-plugin-chains/apis.yaml b/distribution/examples/extending-membrane/reusable-plugin-chains/apis.yaml new file mode 100644 index 0000000000..37a6f6f662 --- /dev/null +++ b/distribution/examples/extending-membrane/reusable-plugin-chains/apis.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.5.json + +components: + log: + chainDef: + flow: + - request: + - log: + message: "Path: ${path}" + cors: + chainDef: + flow: + - response: + - setHeader: + name: Access-Control-Allow-Origin + value: "*" + - setHeader: + name: Access-Control-Allow-Methods + value: POST, PUT +--- + +api: + port: 2000 + path: + uri: /foo + flow: + - chain: + ref: '#/components/log' + - return: + status: 200 +--- + +api: + port: 2000 + path: + uri: /bar + flow: + - chain: + # Chains can be applied to more than one API + ref: '#/components/log' + - chain: + # Referencing long chains can keep API definitions small and comprehensible + ref: '#/components/cors' + - return: + status: 200 diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/ChainExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/ChainExampleTest.java index ba09491058..2f6ac0cc64 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/ChainExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/ChainExampleTest.java @@ -14,11 +14,15 @@ package com.predic8.membrane.examples.withoutinternet; -import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; -import org.junit.jupiter.api.Test; +import com.predic8.membrane.core.http.*; +import com.predic8.membrane.examples.util.*; +import org.jetbrains.annotations.*; +import org.junit.jupiter.api.*; -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.containsString; +import java.util.concurrent.atomic.*; + +import static io.restassured.RestAssured.*; +import static org.junit.jupiter.api.Assertions.*; public class ChainExampleTest extends AbstractSampleMembraneStartStopTestcase { @@ -27,27 +31,45 @@ protected String getExampleDirName() { return "/extending-membrane/reusable-plugin-chains"; } - // @formatter:off + @Test - public void request1() { + void request1() { + AtomicBoolean pathFound = addPathWatcher(); + + // @formatter:off given() .when() - .get("http://localhost:2000") + .get("http://localhost:2000/foo") .then() .assertThat() - .body(containsString("CORS headers applied")) .statusCode(200); + // @formatter:on + + assertTrue(pathFound.get()); } @Test - public void request2() { + void request2() { + AtomicBoolean pathFound = addPathWatcher(); + + // @formatter:off given() .when() - .get("http://localhost:2001") + .get("http://localhost:2000/bar") .then() .assertThat() - .body(containsString("CORS headers applied")) - .statusCode(404); + .header(Header.ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .statusCode(200); + // @formatter:on + + assertTrue(pathFound.get()); + } + + private @NotNull AtomicBoolean addPathWatcher() { + AtomicBoolean pathFound = new AtomicBoolean(); + process.addConsoleWatcher((error, line) -> { + if (line.contains("Path:")) pathFound.set(true); + }); + return pathFound; } - // @formatter:on } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8bf9dfa474..7699fed2de 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,6 +48,16 @@ - Maybe move it to configuration - Register JSON Schema for YAML at: https://www.schemastore.org - Grafana Dashboard: Complete Dashboard for Membrane with documentation in examples/monitoring/grafana +- Remove GroovyTemplateInterceptor (Not Template Interceptor) + - Old an unused +- Configuration independent lookup of beans. I just want bean foo and I do not care where it is defined. + - See: ChainInterceptor.getBean(String) + - Maybe a BeanRegistry implementation for Spring? + +# 7.0.4 + +- Discuss renaming the WebSocketInterceptor.flow to something else to avoid confusion with flowParser +- do not pass a `Router` reference into all sorts of beans: Access to global functionality should happen only on a very limited basis. # 7.0.1