Skip to content

Commit 9fb2657

Browse files
Xpath builtin function and readme yaml (#2485)
* feat: move SpEL built-in functions to CommonBuiltInFunctions; add XPath support and tests - Removed `BuiltInFunctions` and integrated its logic into `CommonBuiltInFunctions` for better modularity. - Introduced support for XPath evaluation in `CommonBuiltInFunctions` using `javax.xml.xpath`. - Updated SpEL and Groovy built-in functions to include XPath support. - Added comprehensive unit tests for XPath functionality and updated README with usage examples. * add `@SuppressWarnings("unused")` to Groovy and SpEL built-in functions and optimize imports in SpEL test * make `SpELBuiltInFunctions#jsonPath` method static for better utility usage * refactor: centralize error handling in `AbstractSetterInterceptor` and enhance `ExchangeExpressionException` with detailed messages and stacktrace control - Moved redundant error handling logic in `AbstractSetterInterceptor` to a dedicated method for improved readability and maintainability. - Updated `ExchangeExpressionException` to support detailed error messages and enable/disable stacktraces. - Introduced `getBuiltInFunctionNames` in `SpELBuiltInFunctions` to provide a list of supported SpEL functions for use in error messages. - Enhanced logging clarity for expression evaluation errors. * refactor: remove `stacktrace` property and simplify error handling in `ExchangeExpressionException` - Eliminated unused `stacktrace` field and its associated methods. - Updated `ExchangeExpressionException` and related classes to streamline error reporting. - Fixed minor wording in README example configuration. * fix: replace string matching with `getMessageCode` in SpEL exception handling for improved clarity - Used `METHOD_NOT_FOUND` constant instead of string matching for detecting method-not-found errors in SpEL expressions. --------- Co-authored-by: Christian Gördes <118011644+christiangoerdes@users.noreply.github.com>
1 parent 6275355 commit 9fb2657

14 files changed

Lines changed: 206 additions & 219 deletions

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -747,18 +747,18 @@ api:
747747
748748
### Transform XML into Text or JSON
749749
750-
Using `setProperty` you can extract values from XML request or response bodies and store it in properties. Then the properties are available as variables inside the `template` plugin.
751-
plugin.
750+
You can use XPath to extract values from an XML message and insert them into a `template`.
752751
753-
```xml
754-
755-
<api port="2000">
756-
<request>
757-
<setProperty name="fn" value="${/person/@firstname}" language="xpath"/>
758-
<template>Buenas Noches, ${property.fn}sito!</template>
759-
</request>
760-
<return/>
761-
</api>
752+
```yaml
753+
api:
754+
port: 2000
755+
flow:
756+
- request:
757+
- template:
758+
src: |
759+
Buenas noches, ${fn.xpath('/person/@firstname')}
760+
- return:
761+
status: 200
762762
```
763763
764764
See: [message-transformation examples](./distribution/examples/message-transformation)
@@ -876,7 +876,7 @@ Membrane offers lots of security features to protect backend servers.
876876
You can define APIs keys directly in your configuration, and Membrane will validate incoming requests against them.
877877
878878
### Example Configuration
879-
The following configuration secures the `Fruitshop API` by validating a key provided as a query parameter:
879+
The following configuration secures the `Fruitshop API` by validating an API key provided as a query parameter:
880880
881881
```xml
882882
<api port="2000">

core/src/main/java/com/predic8/membrane/core/exceptions/ProblemDetails.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public class ProblemDetails {
5454
public static final String MESSAGE = "message";
5555
public static final String STACK_TRACE = "stackTrace";
5656
public static final String LOG_KEY = "logKey";
57+
public static final String DEVELOPMENT_MODE_WARNING = """
58+
Membrane is in development mode. For production set <router production="true"> to reduce details in error messages!""";
5759

5860
/**
5961
* If router is in production mode that should not expose internal details
@@ -136,9 +138,6 @@ public static ProblemDetails problemDetails(String type, boolean production) {
136138
/**
137139
* type/subtype/subtype/...
138140
* lowercase, dash as separator
139-
*
140-
* @param subType
141-
* @return
142141
*/
143142
public ProblemDetails addSubType(String subType) {
144143
this.subType += "/" + subType;
@@ -300,8 +299,7 @@ private Map<String, Object> createInternal(String type) {
300299
}
301300
}
302301
internalMap.put(SEE, getFullType(type));
303-
internalMap.put(ATTENTION, """
304-
Membrane is in development mode. For production set <router production="true"> to reduce details in error messages!""");
302+
internalMap.put(ATTENTION, DEVELOPMENT_MODE_WARNING);
305303
return internalMap;
306304
}
307305

core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractSetterInterceptor.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
package com.predic8.membrane.core.interceptor.lang;
1616

1717
import com.predic8.membrane.annot.*;
18+
import com.predic8.membrane.core.exceptions.*;
1819
import com.predic8.membrane.core.exchange.*;
1920
import com.predic8.membrane.core.interceptor.*;
21+
import com.predic8.membrane.core.lang.*;
2022
import com.predic8.membrane.core.util.*;
2123
import org.slf4j.*;
2224

@@ -51,25 +53,32 @@ private Outcome handleInternal(Exchange exchange, Flow flow) {
5153
try {
5254
setValue(exchange, flow, exchangeExpression.evaluate(exchange, flow, getExpressionReturnType()));
5355
} catch (Exception e) {
54-
var root = ExceptionUtil.getRootCause(e);
55-
var message = "While evaluating expression %s for field %s: %s".formatted(expression, fieldName, root.getMessage());
56-
log.info(message);
57-
56+
var msg = "Error evaluating expression %s for field %s".formatted(expression, fieldName);
5857
if (failOnError) {
59-
internal(getRouter().isProduction(), getDisplayName())
60-
.title("Error evaluating expression!")
61-
.internal("field", fieldName)
62-
.internal("expression", expression)
63-
.exception(root)
58+
if (e instanceof ExchangeExpressionException eee) {
59+
var pd = prepareProblemDetails(msg);
60+
eee.provideDetails(pd);
61+
pd.buildAndSetResponse(exchange);
62+
return ABORT;
63+
}
64+
prepareProblemDetails(msg)
65+
.exception(ExceptionUtil.getRootCause(e))
6466
.stacktrace(false)
6567
.buildAndSetResponse(exchange);
6668
return ABORT;
6769
}
68-
log.info("Error evaluating {} but 'FailOnError' is false therefore ignoring: {}", expression, message);
70+
log.info("'FailOnError' is false therefore ignoring: {}", msg);
6971
}
7072
return CONTINUE;
7173
}
7274

75+
private ProblemDetails prepareProblemDetails(String msg) {
76+
return internal(getRouter().isProduction(), getDisplayName())
77+
.title(msg)
78+
.internal("field", fieldName)
79+
.internal("expression", expression);
80+
}
81+
7382
protected abstract Class<?> getExpressionReturnType();
7483

7584
protected abstract boolean shouldSetValue(Exchange exchange, Flow flow);

core/src/main/java/com/predic8/membrane/core/lang/CommonBuiltInFunctions.java

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,28 @@
1414

1515
package com.predic8.membrane.core.lang;
1616

17-
import com.fasterxml.jackson.databind.ObjectMapper;
18-
import com.jayway.jsonpath.JsonPath;
19-
import com.predic8.membrane.core.exchange.Exchange;
20-
import com.predic8.membrane.core.http.Message;
21-
import com.predic8.membrane.core.interceptor.AbstractInterceptorWithSession;
17+
import com.fasterxml.jackson.databind.*;
18+
import com.jayway.jsonpath.*;
19+
import com.predic8.membrane.core.exchange.*;
20+
import com.predic8.membrane.core.http.*;
21+
import com.predic8.membrane.core.interceptor.*;
2222
import com.predic8.membrane.core.interceptor.Interceptor.Flow;
23-
import com.predic8.membrane.core.security.BasicHttpSecurityScheme;
24-
import com.predic8.membrane.core.security.SecurityScheme;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
27-
28-
import java.util.Collection;
29-
import java.util.List;
30-
import java.util.Map;
31-
import java.util.Optional;
32-
import java.util.concurrent.ThreadLocalRandom;
23+
import com.predic8.membrane.core.lang.spel.*;
24+
import com.predic8.membrane.core.security.*;
25+
import com.predic8.membrane.core.util.xml.*;
26+
import com.predic8.membrane.core.util.xml.parser.*;
27+
import org.slf4j.*;
28+
29+
import javax.xml.xpath.*;
30+
import java.util.*;
31+
import java.util.concurrent.*;
3332
import java.util.function.Predicate;
3433

35-
import static com.predic8.membrane.core.exchange.Exchange.SECURITY_SCHEMES;
36-
import static com.predic8.membrane.core.http.Header.AUTHORIZATION;
37-
import static java.nio.charset.StandardCharsets.UTF_8;
38-
import static java.util.Collections.emptyList;
39-
import static java.util.Objects.requireNonNull;
34+
import static com.predic8.membrane.core.exchange.Exchange.*;
35+
import static com.predic8.membrane.core.http.Header.*;
36+
import static java.nio.charset.StandardCharsets.*;
37+
import static java.util.Collections.*;
38+
import static java.util.Objects.*;
4039

4140
/**
4241
* Place to share built-in functions between SpEL and Groovy.
@@ -47,6 +46,9 @@ public class CommonBuiltInFunctions {
4746

4847
private static final ObjectMapper objectMapper = new ObjectMapper();
4948

49+
private static final XPathFactory xPathFactory = XPathFactory.newInstance();
50+
private static final XmlParser parser = HardenedXmlParser.getInstance();
51+
5052
public static Object jsonPath(String jsonPath, Message msg) {
5153
try {
5254
return JsonPath.read(objectMapper.readValue(msg.getBodyAsStringDecoded(), Map.class), jsonPath);
@@ -55,9 +57,25 @@ public static Object jsonPath(String jsonPath, Message msg) {
5557
}
5658
}
5759

60+
public static String xpath(String xpath, Message message) {
61+
XPath xPath = xPathFactory.newXPath();
62+
63+
// TODO: Leave the comment in till the XML namespace support is realized!
64+
// - When there is the new registry use it to obtain the XMLConfig.
65+
// if (xmlConfig != null && xmlConfig.getNamespaces() != null) {
66+
// xPath.setNamespaceContext(xmlConfig.getNamespaces().getNamespaceContext());
67+
// }
68+
69+
try {
70+
return xPath.evaluate(xpath, parser.parse(XMLUtil.getInputSource(message)), XPathConstants.STRING).toString();
71+
} catch (XPathExpressionException ignored) {
72+
return null;
73+
}
74+
}
75+
5876
public static String user(Exchange exchange) {
59-
List<SecurityScheme> schemes = exchange.getProperty(SECURITY_SCHEMES, List.class );
60-
for (SecurityScheme scheme :schemes) {
77+
List<SecurityScheme> schemes = exchange.getProperty(SECURITY_SCHEMES, List.class);
78+
for (SecurityScheme scheme : schemes) {
6179
if (scheme instanceof BasicHttpSecurityScheme basic) {
6280
return basic.getUsername();
6381
}
@@ -100,7 +118,7 @@ public static String base64Encode(String s) {
100118

101119
public static boolean isBearerAuthorization(Exchange exc) {
102120
return exc.getRequest().getHeader().contains(AUTHORIZATION)
103-
&& exc.getRequest().getHeader().getFirstValue(AUTHORIZATION).startsWith("Bearer");
121+
&& exc.getRequest().getHeader().getFirstValue(AUTHORIZATION).startsWith("Bearer");
104122
}
105123

106124
private static List<String> getSchemeScopes(Predicate<SecurityScheme> predicate, Exchange exc) {

core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpressionException.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ public class ExchangeExpressionException extends RuntimeException {
2121

2222
private final String expression;
2323
private final Map<String, Object> extensions = new HashMap<>();
24-
25-
private boolean statcktrace = true;
24+
private String detail;
2625

2726
/*
2827
* Body that cause the error
@@ -39,8 +38,9 @@ public ExchangeExpressionException(String expression, Throwable cause) {
3938
* @return ProblemDetails filled from exception
4039
*/
4140
public ProblemDetails provideDetails(ProblemDetails pd) {
42-
pd.internal("expression", expression)
43-
.stacktrace(statcktrace);
41+
if (detail != null)
42+
pd.detail(detail);
43+
pd.internal("expression", expression);
4444
for (Map.Entry<String, Object> entry : extensions.entrySet()) {
4545
pd.internal(entry.getKey(), entry.getValue());
4646
}
@@ -51,6 +51,11 @@ public ProblemDetails provideDetails(ProblemDetails pd) {
5151
return pd;
5252
}
5353

54+
public ExchangeExpressionException detail(String detail) {
55+
this.detail = detail;
56+
return this;
57+
}
58+
5459
public ExchangeExpressionException extension(String key, Object value) {
5560
extensions.put(key, value);
5661
return this;
@@ -66,11 +71,6 @@ public ExchangeExpressionException column(String column) {
6671
return this;
6772
}
6873

69-
public ExchangeExpressionException stacktrace(boolean stacktrace) {
70-
this.statcktrace = stacktrace;
71-
return this;
72-
}
73-
7474
public ExchangeExpressionException body(String body) {
7575
this.body = body;
7676
return this;

core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyBuiltInFunctions.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* Difference to SpEL:
2727
* The functions are called with ${fn.functionname()} instead of ${functionname()} in the template interceptor
2828
*/
29+
@SuppressWarnings("unused")
2930
public class GroovyBuiltInFunctions extends GroovyObjectSupport {
3031

3132
private final Exchange exchange;
@@ -45,6 +46,10 @@ public Object jsonPath(String jsonPath) {
4546
return CommonBuiltInFunctions.jsonPath(jsonPath, exchange.getMessage(flow));
4647
}
4748

49+
public String xpath(String xpath) {
50+
return CommonBuiltInFunctions.xpath(xpath, exchange.getMessage(flow));
51+
}
52+
4853
public List<String> scopes() {
4954
return CommonBuiltInFunctions.scopes(exchange);
5055
}
@@ -69,6 +74,8 @@ public boolean isXML() {
6974
return CommonBuiltInFunctions.isXML(exchange, flow);
7075
}
7176

77+
78+
7279
public boolean isJSON() {
7380
return CommonBuiltInFunctions.isJSON(exchange, flow);
7481
}

core/src/main/java/com/predic8/membrane/core/lang/spel/SpELExchangeExpression.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import static java.lang.Boolean.*;
3232
import static org.springframework.expression.spel.SpelCompilerMode.*;
33+
import static org.springframework.expression.spel.SpelMessage.METHOD_NOT_FOUND;
3334

3435
public class SpELExchangeExpression extends AbstractExchangeExpression {
3536

@@ -82,13 +83,19 @@ public <T> T evaluate(Exchange exchange, Flow flow, Class<T> type) {
8283
}
8384
throw new RuntimeException("Cannot cast %s to %s".formatted(o,type));
8485
} catch (SpelEvaluationException see) {
85-
log.error("Error in expression '{}': {}",expression, see.getLocalizedMessage());
8686
ExchangeExpressionException eee = new ExchangeExpressionException(expression, see);
87-
if (see.getCause() instanceof ConverterNotFoundException cnfe) {
87+
String msg ;
88+
if (see.getMessageCode() == METHOD_NOT_FOUND) {
89+
msg = "Method not found in expression '%s' use a SpEL function or one of Membrane's: %s".formatted(expression, SpELBuiltInFunctions.getBuiltInFunctionNames());
90+
} else if (see.getCause() instanceof ConverterNotFoundException cnfe) {
91+
msg = "Type converter not found for expression '%s' from '%s' to '%s'.".formatted(expression, cnfe.getSourceType(), cnfe.getTargetType());
8892
eee.extension("sourceType", cnfe.getSourceType())
8993
.extension("targetType", cnfe.getTargetType());
94+
} else {
95+
msg = "Error in expression '%s': %s".formatted( expression, see.getMessage());
9096
}
91-
eee.stacktrace(false);
97+
log.warn(msg);
98+
eee.detail(msg);
9299
throw eee;
93100
}
94101
}

0 commit comments

Comments
 (0)