Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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())) {
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ public interface BeanRegistry {
*/
<T> Optional<T> getBean(Class<T> 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 <T> the bean type
*/
<T> Optional<T> getBean(String name, Class<T> clazz);

/**
* Registers a bean with the specified name.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ public <T> Optional<T> getBean(Class<T> clazz) {
return beans.size() == 1 ? Optional.of(beans.getFirst()) : Optional.empty();
}

public <T> Optional<T> getBean(String beanname, Class<T> clazz) {
return getFirstByName(beanname)
.map(bc -> bc.getOrCreate(this, grammar))
.map(clazz::cast);
}
Comment thread
predic8 marked this conversation as resolved.

public void register(String beanName, Object bean) {
if (bean == null)
throw new IllegalArgumentException("bean must not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public <T> Optional<T> getBean(Class<T> clazz) {
throw new UnsupportedOperationException();
}

@Override
public <T> Optional<T> getBean(String name, Class<T> clazz) {
throw new UnsupportedOperationException();
}
Comment thread
rrayst marked this conversation as resolved.

@Override
public void register(String beanName, Object bean) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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 <T> Method getAnySetter(Class<T> clazz) {
return stream(clazz.getMethods())
.filter(McYamlIntrospector::isSetter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -42,9 +39,28 @@ public void init() {
}

private List<Interceptor> 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 <T> Optional<T> getBean(String name, Class<T> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ public void setRegistry(BeanRegistry registry) {

@Override
public BeanRegistry getRegistry() {
return mainComponents.getRegistry();
return mainComponents.getRegistry();
}

public void applyConfiguration(Configuration configuration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ public <T> Optional<T> getBean(Class<T> clazz) {
return Optional.empty();
}

@Override
public <T> Optional<T> getBean(String beanName, Class<T> clazz) {
return Optional.empty();
}

@Override
public void register(String beanName, Object object) {}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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());
Comment thread
predic8 marked this conversation as resolved.
}

@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
}
10 changes: 10 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
rrayst marked this conversation as resolved.


# 7.0.1
Expand Down
Loading