Skip to content

Commit c981cd8

Browse files
authored
Merge branch 'master' into mongodb-exchange-store-yaml-example
2 parents d5b223b + 3773a7d commit c981cd8

36 files changed

Lines changed: 251 additions & 227 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,11 @@ api:
297297
#### Viewing Deployed APIs
298298
Once configured, a list of deployed APIs is available at: [http://localhost:2000/api-docs](http://localhost:2000/api-docs)
299299

300-
![List of OpenAPI Deployments](distribution/examples/openapi/openapi-proxy/api-overview.png)
300+
![List of OpenAPI Deployments](distribution/examples/openapi/openapi-proxy/api-overview.jpg)
301301

302302
Click on an API title in the list to open the Swagger UI for interactive exploration and testing:
303303

304-
![Swagger UI](distribution/examples/openapi/openapi-proxy/swagger-ui.png)
304+
![Swagger UI](distribution/examples/openapi/openapi-proxy/swagger-ui.jpg)
305305

306306
### Learn More
307307
For additional details and a working example, check out the [OpenAPI Example](distribution/examples/openapi).

annot/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<dependency>
5555
<groupId>com.networknt</groupId>
5656
<artifactId>json-schema-validator</artifactId>
57-
<version>2.0.0</version>
57+
<version>2.0.1</version>
5858
</dependency>
5959
<dependency>
6060
<groupId>com.github.spotbugs</groupId>

annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,21 @@
1414

1515
package com.predic8.membrane.annot.yaml;
1616

17-
import com.fasterxml.jackson.databind.JsonNode;
18-
import com.predic8.membrane.annot.MCChildElement;
17+
import com.fasterxml.jackson.databind.*;
18+
import com.predic8.membrane.annot.*;
1919
import org.jetbrains.annotations.*;
2020

21-
import javax.lang.model.util.Types;
21+
import javax.lang.model.util.*;
2222
import java.lang.reflect.*;
23-
import java.util.Collection;
24-
import java.util.List;
25-
import java.util.Map;
23+
import java.util.*;
2624

27-
import static com.predic8.membrane.annot.yaml.GenericYamlParser.createAndPopulateNode;
28-
import static com.predic8.membrane.annot.yaml.GenericYamlParser.parseListIncludingStartEvent;
25+
import static com.predic8.membrane.annot.yaml.GenericYamlParser.*;
2926
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*;
30-
import static java.lang.Boolean.parseBoolean;
31-
import static java.lang.Integer.parseInt;
32-
import static java.lang.Long.parseLong;
33-
import static java.util.Locale.ROOT;
27+
import static java.lang.Boolean.*;
28+
import static java.lang.Double.*;
29+
import static java.lang.Integer.*;
30+
import static java.lang.Long.*;
31+
import static java.util.Locale.*;
3432

3533
public class MethodSetter {
3634

@@ -85,27 +83,53 @@ public <T> void setSetter(T instance, ParsingContext ctx, JsonNode node, String
8583
private Object resolveSetterValue(ParsingContext ctx, JsonNode node, String key) throws WrongEnumConstantException, ParsingException {
8684
Class<?> wanted = getParameterType();
8785

86+
// Collections / repeated elements
8887
List<Object> list = getObjectList(ctx, node, key, wanted);
8988
if (list != null) return list;
9089

90+
// Structured objects
91+
if (McYamlIntrospector.isStructured(setter)) {
92+
if (beanClass != null) return createAndPopulateNode(ctx.updateContext(key), beanClass, node);
93+
return createAndPopulateNode(ctx.updateContext(key), wanted, node);
94+
}
95+
96+
return coerceScalarOrReference(ctx, node, key, wanted);
97+
}
98+
99+
/**
100+
* Attempts to coerce a given JSON node into the desired scalar, enum, reference, or map type,
101+
* as specified by the provided target class.
102+
*
103+
* @param ctx The parsing context, providing access to type resolution and bean lookup mechanisms.
104+
* @param node The JSON node to be coerced into the desired type.
105+
* @param key The key corresponding to the JSON node, often used for error messages or map assignments.
106+
* @param wanted The target class specifying the type into which the node should be converted.
107+
* @return The coerced object, matching the desired type, derived from the input node.
108+
* @throws WrongEnumConstantException If the node value does not match any of the constants in the enum type.
109+
* @throws ParsingException If the provided type is unsupported for coercion or other unexpected issues arise.
110+
*/
111+
Object coerceScalarOrReference(ParsingContext ctx, JsonNode node, String key, Class<?> wanted) throws WrongEnumConstantException {
112+
// Scalars, enums, bean refs, "other attributes"
91113
if (wanted.isEnum()) return parseEnum(wanted, node);
92114
if (wanted.equals(String.class)) return node.asText();
93115

94-
if (wanted == Integer.TYPE || wanted == Integer.class) return parseInt(node.asText());
95-
if (wanted == Long.TYPE || wanted == Long.class) return parseLong(node.asText());
96-
if (wanted == Boolean.TYPE || wanted == Boolean.class) return parseBoolean(node.asText());
97-
if (wanted.equals(Map.class) && McYamlIntrospector.hasOtherAttributes(setter)) return Map.of(key, node.asText());
116+
if (wanted == int.class || wanted == Integer.class)
117+
return node.isInt() ? node.intValue() : parseInt(node.asText());
118+
if (wanted == long.class || wanted == Long.class)
119+
return node.isLong() || node.isInt() ? node.longValue() : parseLong(node.asText());
120+
if (wanted == double.class || wanted == Double.class)
121+
return node.isNumber() ? node.doubleValue() : parseDouble(node.asText());
122+
if (wanted == boolean.class || wanted == Boolean.class)
123+
return node.isBoolean() ? node.booleanValue() : parseBoolean(node.asText());
124+
if (wanted.equals(Map.class) && McYamlIntrospector.hasOtherAttributes(setter))
125+
return Map.of(key, node.asText());
98126

99127
if (node.isTextual() && isBeanReference(wanted)) {
100128
return resolveReference(ctx, node, key, wanted);
101129
}
102130

103-
if (McYamlIntrospector.isStructured(setter)) {
104-
if (beanClass != null) return createAndPopulateNode(ctx.updateContext(key), beanClass, node);
105-
return createAndPopulateNode(ctx.updateContext(key), wanted, node);
106-
}
107131
if (McYamlIntrospector.isReferenceAttribute(setter)) return ctx.registry().resolve(node.asText());
108-
throw new RuntimeException("Not implemented setter type " + wanted);
132+
throw new ParsingException("Unsupported setter type: %s for key '%s' with node type %s".formatted(wanted.getName(), key, node.getNodeType()), node);
109133
}
110134

111135
private @Nullable List<Object> getObjectList(ParsingContext ctx, JsonNode node, String key, Class<?> wanted) {
@@ -118,7 +142,7 @@ private Object resolveSetterValue(ParsingContext ctx, JsonNode node, String key)
118142
if (o == null) continue;
119143
if (!elemType.isAssignableFrom(o.getClass())) {
120144
throw new ParsingException("Value of type '%s' is not allowed in list '%s'. Expected '%s'."
121-
.formatted(McYamlIntrospector.getElementName(o.getClass()), key, elemType.getSimpleName()), node);
145+
.formatted(McYamlIntrospector.getElementName(o.getClass()), key, elemType.getSimpleName()), node);
122146
}
123147
}
124148
}

annot/src/test/java/com/predic8/membrane/annot/yaml/MethodSetterTest.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@
1414

1515
package com.predic8.membrane.annot.yaml;
1616

17-
import com.predic8.membrane.annot.MCChildElement;
18-
import com.predic8.membrane.annot.util.GrammarMock;
19-
import org.junit.jupiter.api.Test;
17+
import com.fasterxml.jackson.core.*;
18+
import com.fasterxml.jackson.databind.*;
19+
import com.predic8.membrane.annot.*;
20+
import com.predic8.membrane.annot.util.*;
21+
import org.junit.jupiter.api.*;
2022

21-
import static com.predic8.membrane.annot.yaml.MethodSetter.getMethodSetter;
23+
import static com.predic8.membrane.annot.yaml.MethodSetter.*;
2224
import static org.junit.jupiter.api.Assertions.*;
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
2326

2427
class MethodSetterTest {
2528

29+
private static final ObjectMapper om = new ObjectMapper();
30+
2631
@SuppressWarnings("unused")
2732
static class A {
2833
public void setA1(B b) {}
@@ -44,25 +49,36 @@ public static class B {}
4449
public static class C {}
4550

4651
@Test
47-
public void dontUseMethodsWithoutChildElementAnnotation() {
52+
void dontUseMethodsWithoutChildElementAnnotation() {
4853
MethodSetter ms = getMethodSetter(new ParsingContext("foo", null,
4954
new GrammarMock().withGlobalElement("b", B.class)),
5055
A.class, "b");
5156
assertEquals("setA3", ms.getSetter().getName());
5257
}
5358

5459
@Test
55-
public void multiplePotentialSettersFound() {
60+
void multiplePotentialSettersFound() {
5661
assertThrowsExactly(RuntimeException.class, () -> getMethodSetter(new ParsingContext("foo", null,
5762
new GrammarMock().withGlobalElement("b", B.class)),
5863
A2.class, "b"));
5964
}
6065

6166
@Test
62-
public void noPotentialSetterFound() {
67+
void noPotentialSetterFound() {
6368
assertThrowsExactly(RuntimeException.class, () -> getMethodSetter(new ParsingContext("foo", null,
6469
new GrammarMock().withGlobalElement("c", C.class)),
6570
A2.class, "c"));
6671
}
6772

73+
@Test
74+
void coerceScalar() throws Exception {
75+
var ms = new MethodSetter(null, null);
76+
assertEquals(true, ms.coerceScalarOrReference(null, om.readTree("true"), null, boolean.class));
77+
assertEquals(true, ms.coerceScalarOrReference(null, om.readTree("true"), null, Boolean.class));
78+
assertEquals(1, ms.coerceScalarOrReference(null, om.readTree("1"), null, int.class));
79+
assertEquals(1.0, ms.coerceScalarOrReference(null, om.readTree("1"), null, double.class));
80+
var l = ms.coerceScalarOrReference(null, om.readTree("1"), null, long.class);
81+
assertInstanceOf(Long.class, l);
82+
assertEquals(1L, l);
83+
}
6884
}

core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ private static void start(String[] args) {
7575
if (commandLine.getCommand().getName().equals("private-jwk-to-public")) {
7676
privateJwkToPublic(commandLine);
7777
}
78-
var router = getRouter(commandLine);
79-
if (router instanceof DefaultRouter dr)
78+
if (getRouter(commandLine) instanceof DefaultRouter dr)
8079
dr.waitFor();
8180
}
8281

core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
package com.predic8.membrane.core.interceptor.apikey;
1515

1616
import com.predic8.membrane.annot.*;
17-
import com.predic8.membrane.core.config.spring.*;
1817
import com.predic8.membrane.core.exchange.*;
1918
import com.predic8.membrane.core.interceptor.*;
2019
import com.predic8.membrane.core.interceptor.apikey.extractors.*;
@@ -23,11 +22,10 @@
2322
import org.slf4j.*;
2423

2524
import java.util.*;
26-
import java.util.stream.*;
2725

2826
import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
2927
import static com.predic8.membrane.core.interceptor.Outcome.*;
30-
import static java.util.stream.Collectors.joining;
28+
import static java.util.stream.Collectors.*;
3129
import static java.util.stream.Stream.*;
3230

3331
/**
@@ -85,10 +83,17 @@ public String getLongDescription() {
8583
@Override
8684
public void init() {
8785
super.init();
88-
// At the moment the beanFactory is only there when the Membrane configuration was read from XML
86+
87+
// Todo: Move logic into the registry
88+
// The beanFactory is only there when the Membrane configuration was read from XML
8989
if (router.getBeanFactory() != null) {
9090
stores.addAll(router.getBeanFactory().getBeansOfType(ApiKeyStore.class).values());
9191
}
92+
// For YAML configuration
93+
if (router.getRegistry() != null) {
94+
this.stores.addAll(router.getRegistry().getBeans(ApiKeyStore.class));
95+
}
96+
9297
stores.forEach(s -> s.init(router));
9398

9499
// Add the default extractor if none is configured

core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static com.predic8.membrane.core.security.ApiKeySecurityScheme.In.*;
2323

2424
/**
25-
* @deprecated Set an expression like ${header['api']} on apiKey
2625
* @description Extracts an API key from a specific HTTP request header. By default, the header name
2726
* is <code>X-Api-Key</code>. If the header is present, its first value is returned as the API key.
2827
* <p>

core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Key.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@
2626
@MCElement(name = "secret", component = false)
2727
public class Key {
2828

29-
private final List<Scope> scopes = new ArrayList<>();
29+
private final List<String> scopes = new ArrayList<>();
3030

3131
private String value;
3232

3333
/**
34-
* @description <scope>...</scope> elements for defining scopes for this key.
34+
* @description Scopes for this key.
3535
*/
3636
@MCChildElement(allowForeign = true)
37-
public void setScopes(List<Scope> scopes) {
37+
public void setScopes(List<String> scopes) {
3838
this.scopes.addAll(scopes);
3939
}
4040

41-
public List<Scope> getScopes() {
41+
public List<String> getScopes() {
4242
return scopes;
4343
}
4444

core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/Scope.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

core/src/main/java/com/predic8/membrane/core/interceptor/apikey/stores/inConfig/SimpleKeyStore.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ public List<Key> getKeys() {
4545
public Optional<Set<String>> getScopes(String apiKey) throws UnauthorizedApiKeyException {
4646
var key = keys.stream().filter(k -> k.getValue().equals(apiKey)).findFirst();
4747
if (key.isPresent()) {
48-
Set<String> scopeValues = key.get().getScopes().stream()
49-
.map(Scope::getValue)
50-
.collect(toSet());
48+
Set<String> scopeValues = new HashSet<>(key.get().getScopes());
5149
return ofNullable(scopeValues.isEmpty() ? null : scopeValues);
5250
} else {
5351
throw new UnauthorizedApiKeyException();

0 commit comments

Comments
 (0)