Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public interface BeanRegistry {
*/
<T> Optional<T> getBean(Class<T> clazz);

/**
* Retrieves a bean with the specified name.
* @param beanName
* @param clazz
* @return Optional containing the bean
* @param <T> the bean type
*/
<T> Optional<T> getBean(String beanName, Class<T> clazz);

/**
* Registers a bean with the specified name.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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);
}

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 @@ -57,6 +57,13 @@ public MethodSetter(Method setter, Class<?> beanClass) {
}
Class<?> beanClass = null;
if (setter == null) {

// TODO: Find more robust solution than this workaround.
// If object is a child of Component use this shortcut. Otherwise there is a problem, if the name of the component is a name of a configuration element like log, request, flow, ...
if ("com.predic8.membrane.core.config.spring.Components".equals(clazz.getName())) {
return new MethodSetter(getAnySetter(clazz), beanClass);
}

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 @@ -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 @@ -366,6 +366,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());
}

@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
}
3 changes: 3 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
- 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

Expand Down
Loading