From ce94e262351928aeb70b74afdff948b2f8c989b4 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 1 Nov 2025 17:02:04 +0100 Subject: [PATCH 01/16] feat: XML namespace declarations for XPath --- .../predic8/membrane/core/http/Request.java | 6 + .../core/interceptor/XMLNamespaceSupport.java | 10 ++ .../extractors/ApiKeyExpressionExtractor.java | 28 +++-- .../balancer/PolyglotSessionIdExtractor.java | 6 +- .../core/interceptor/flow/ForInterceptor.java | 2 +- .../core/interceptor/flow/IfInterceptor.java | 20 +++- .../core/interceptor/flow/choice/Case.java | 28 +++-- .../idempotency/IdempotencyInterceptor.java | 21 +--- ...AbstractExchangeExpressionInterceptor.java | 3 +- .../lang/AbstractLanguageInterceptor.java | 20 +++- .../lang/AbstractSetterInterceptor.java | 15 ++- .../core/interceptor/lang/Namespaces.java | 103 ++++++++++++++++++ .../ratelimit/RateLimitInterceptor.java | 2 +- .../soap/SampleSoapServiceInterceptor.java | 2 +- .../core/lang/ExchangeExpression.java | 41 ++++++- .../core/lang/TemplateExchangeExpression.java | 13 ++- .../lang/groovy/GroovyExchangeExpression.java | 4 +- .../lang/xpath/XPathExchangeExpression.java | 31 +++++- .../core/openapi/serviceproxy/APIProxy.java | 22 +++- .../core/proxies/AbstractServiceProxy.java | 23 +++- .../membrane/core/util/ExceptionUtil.java | 12 ++ .../predic8/membrane/core/util/SOAPUtil.java | 2 +- .../membrane/core/util/{ => xml}/XMLUtil.java | 2 +- .../util/xml/parser/HardenedXmlParser.java | 89 +++++++++++++++ .../util/xml/parser/XmlParseException.java | 34 ++++++ .../core/util/xml/parser/XmlParser.java | 34 ++++++ .../core/interceptor/lang/NamespacesTest.java | 42 +++++++ .../lang/AbstractExchangeExpressionTest.java | 6 +- .../lang/TemplateExchangeExpressionTest.java | 4 +- .../JsonpathExchangeExpressionTest.java | 4 +- .../xpath/XPathExchangeExpressionTest.java | 26 +++++ .../membrane/core/util/XMLUtilTest.java | 1 + .../examples/templating/json/README.md | 6 +- .../examples/xml/namespaces/README.md | 16 +++ .../examples/xml/namespaces/membrane.cmd | 17 +++ .../examples/xml/namespaces/membrane.sh | 21 ++++ .../examples/xml/namespaces/person.xml | 6 + .../examples/xml/namespaces/proxies.xml | 54 +++++++++ .../examples/xml/namespaces/requests.http | 9 ++ .../test/VersioningSoapXsltExampleTest.java | 3 + .../xml/namespaces/NamespacesExampleTest.java | 31 ++++++ docs/ROADMAP.md | 2 + 42 files changed, 736 insertions(+), 85 deletions(-) create mode 100644 core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java create mode 100644 core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java rename core/src/main/java/com/predic8/membrane/core/util/{ => xml}/XMLUtil.java (98%) create mode 100644 core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java create mode 100644 core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParseException.java create mode 100644 core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParser.java create mode 100644 core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java create mode 100644 distribution/examples/xml/namespaces/README.md create mode 100644 distribution/examples/xml/namespaces/membrane.cmd create mode 100755 distribution/examples/xml/namespaces/membrane.sh create mode 100644 distribution/examples/xml/namespaces/person.xml create mode 100644 distribution/examples/xml/namespaces/proxies.xml create mode 100644 distribution/examples/xml/namespaces/requests.http create mode 100644 distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java diff --git a/core/src/main/java/com/predic8/membrane/core/http/Request.java b/core/src/main/java/com/predic8/membrane/core/http/Request.java index 7c3bd0d700..3f3724ee77 100644 --- a/core/src/main/java/com/predic8/membrane/core/http/Request.java +++ b/core/src/main/java/com/predic8/membrane/core/http/Request.java @@ -311,6 +311,12 @@ public Builder json(String body) { return this; } + public Builder xml(String body) { + req.setBodyContent(body.getBytes(UTF_8)); + req.header.setContentType(APPLICATION_XML); + return this; + } + public Builder post(URIFactory uriFactory, String url) throws URISyntaxException { return method(Request.METHOD_POST).url(uriFactory, url); } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java new file mode 100644 index 0000000000..27bc7780ad --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java @@ -0,0 +1,10 @@ +package com.predic8.membrane.core.interceptor; + +import com.predic8.membrane.core.interceptor.lang.*; + +public interface XMLNamespaceSupport { + + void setNamespaces(Namespaces namespaces); + + Namespaces getNamespaces(); +} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java index 80e4ab7fbb..6b73367a93 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java @@ -13,13 +13,13 @@ limitations under the License. */ package com.predic8.membrane.core.interceptor.apikey.extractors; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.annot.*; import com.predic8.membrane.core.Router; import com.predic8.membrane.core.exchange.Exchange; -import com.predic8.membrane.core.interceptor.lang.Polyglot; -import com.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; +import com.predic8.membrane.core.lang.*; +import com.predic8.membrane.core.lang.ExchangeExpression.*; import java.util.Optional; @@ -45,15 +45,16 @@ * @topic 3. Security and Validation */ @MCElement(name="expressionExtractor", topLevel = false) -public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot { +public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XMLNamespaceSupport { private String expression = ""; private Language language = SPEL; private ExchangeExpression exchangeExpression; + private Namespaces namespaces; @Override public void init(Router router) { - exchangeExpression = ExchangeExpression.newInstance(router, language, expression); + exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, expression); } @Override @@ -93,4 +94,17 @@ public String getExpression() { public void setExpression(String expression) { this.expression = expression; } + + /** + * Declaration of XML namespaces for XPath expressions. + * @param namespaces + */ + @MCChildElement(allowForeign = true) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java index f3ec2b41c1..b69e3b19ff 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java @@ -21,8 +21,8 @@ import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.Interceptor.Flow; import com.predic8.membrane.core.interceptor.lang.Polyglot; -import com.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +import com.predic8.membrane.core.lang.*; +import com.predic8.membrane.core.lang.ExchangeExpression.*; @MCElement(name = "sessionIdExtractor") public class PolyglotSessionIdExtractor extends AbstractXmlElement implements SessionIdExtractor, Polyglot { @@ -33,7 +33,7 @@ public class PolyglotSessionIdExtractor extends AbstractXmlElement implements Se public void init(Router router) { if (sessionSource != null && !sessionSource.isEmpty()) { - exchangeExpression = ExchangeExpression.newInstance(router, language, sessionSource); + exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router), language, sessionSource); } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java index 49ad0385bf..fb0dd0e199 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java @@ -56,7 +56,7 @@ public class ForInterceptor extends AbstractFlowWithChildrenInterceptor { public void init() { super.init(); try { - exchangeExpression = ExchangeExpression.newInstance(router, language, in); + exchangeExpression = ExchangeExpression.newInstance(this, language, in); } catch (ConfigurationException ce) { throw new ConfigurationException(ce.getMessage() + """ diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java index 881bf33231..77223fce99 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java @@ -17,8 +17,10 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; +import com.predic8.membrane.core.lang.xpath.*; import org.slf4j.*; import static com.predic8.membrane.core.exceptions.ProblemDetails.*; @@ -37,7 +39,7 @@ * @topic 1. Proxies and Flow */ @MCElement(name = "if") -public class IfInterceptor extends AbstractFlowWithChildrenInterceptor { +public class IfInterceptor extends AbstractFlowWithChildrenInterceptor implements XMLNamespaceSupport { private static final Logger log = LoggerFactory.getLogger(IfInterceptor.class); private String test; @@ -52,7 +54,7 @@ public IfInterceptor() { @Override public void init() { super.init(); - exchangeExpression = ExchangeExpression.newInstance(router, language, test); + exchangeExpression = ExchangeExpression.newInstance(this, language, test); } @Override @@ -130,4 +132,18 @@ public String getShortDescription() { ret.append("
}"); return ret.toString(); } + + /** + * XML namespaces to be used in expressions. + */ + protected Namespaces namespaces; + + @MCChildElement(allowForeign = true,order = 10) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java index b71b80dfe6..21ae81686c 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java @@ -13,30 +13,31 @@ limitations under the License. */ package com.predic8.membrane.core.interceptor.flow.choice; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.annot.MCElement; -import com.predic8.membrane.annot.Required; +import com.predic8.membrane.annot.*; import com.predic8.membrane.core.Router; import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.Interceptor.Flow; -import com.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +import com.predic8.membrane.core.interceptor.lang.*; +import com.predic8.membrane.core.lang.*; +import com.predic8.membrane.core.lang.ExchangeExpression.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; @MCElement(name = "case", topLevel = false) -public class Case extends InterceptorContainer { +public class Case extends InterceptorContainer implements XMLNamespaceSupport { private static final Logger log = LoggerFactory.getLogger(Case.class); private String test; private Language language = SPEL; private ExchangeExpression exchangeExpression; + private Namespaces namespaces; public void init(Router router) { - exchangeExpression = ExchangeExpression.newInstance(router, language, test); + exchangeExpression = ExchangeExpression.newInstance( new InterceptorAdapter(router,namespaces), language, test); } boolean evaluate(Exchange exc, Flow flow) { @@ -76,4 +77,17 @@ public String getTest() { public void setTest(String test) { this.test = test; } + + /** + * Declaration of XML namespaces for XPath expressions. + * @param namespaces + */ + @MCChildElement(allowForeign = true,order = 10) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java index 6fe142e3fe..d86962230f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java @@ -22,6 +22,7 @@ import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.AbstractInterceptor; import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression; import com.predic8.membrane.core.lang.ExchangeExpression.Language; @@ -43,18 +44,17 @@ * @topic 3. Security and Validation */ @MCElement(name = "idempotency") -public class IdempotencyInterceptor extends AbstractInterceptor { +public class IdempotencyInterceptor extends AbstractLanguageInterceptor { private String key; private ExchangeExpression exchangeExpression; - private Language language = SPEL; private int expiration = 3600; private Cache processedKeys; @Override public void init() { super.init(); - exchangeExpression = ExchangeExpression.newInstance(router, language, key); + exchangeExpression = ExchangeExpression.newInstance(this, language, key); processedKeys = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(expiration, TimeUnit.SECONDS) @@ -104,17 +104,6 @@ public void setKey(String key) { this.key = key; } - /** - * @description Language used to interpret the expression (e.g., spel, jsonpath, xpath). - * Determines how the key string will be evaluated. - * @default SpEL - * @example jsonpath - */ - @MCAttribute - public void setLanguage(Language language) { - this.language = language; - } - /** * @description Time in seconds after which idempotency keys automatically expire. * Useful to avoid memory leaks in long-running systems. @@ -136,10 +125,6 @@ public String getKey() { return key; } - public Language getLanguage() { - return language; - } - public int getExpiration() { return expiration; } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractExchangeExpressionInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractExchangeExpressionInterceptor.java index 4cd140f45a..250ea95d73 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractExchangeExpressionInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractExchangeExpressionInterceptor.java @@ -27,7 +27,6 @@ public void init() { } protected ExchangeExpression getExchangeExpression() { - return TemplateExchangeExpression.newInstance(router, language, expression); + return TemplateExchangeExpression.newInstance(this, language, expression); } - } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java index d1122a2627..39df993242 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java @@ -14,15 +14,15 @@ package com.predic8.membrane.core.interceptor.lang; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.core.interceptor.AbstractInterceptor; +import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.lang.ExchangeExpression.Language; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; -abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot{ +public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLNamespaceSupport { private static final Logger log = LoggerFactory.getLogger(AbstractLanguageInterceptor.class); @@ -30,6 +30,7 @@ abstract class AbstractLanguageInterceptor extends AbstractInterceptor implement * SpEL is default */ protected Language language = SPEL; + protected Namespaces namespaces; public String getLanguage() { return language.name(); @@ -44,4 +45,17 @@ public String getLanguage() { public void setLanguage(Language language) { this.language = language; } + + /** + * Declaration of XML namespaces for XPath expressions. + * @param namespaces + */ + @MCChildElement(allowForeign = true) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractSetterInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractSetterInterceptor.java index ba8eb9f272..e89e2c4e28 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractSetterInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractSetterInterceptor.java @@ -17,12 +17,13 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.util.*; import org.slf4j.*; import static com.predic8.membrane.core.exceptions.ProblemDetails.*; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; -import static com.predic8.membrane.core.interceptor.Outcome.ABORT; import static com.predic8.membrane.core.interceptor.Outcome.*; +import static com.predic8.membrane.core.interceptor.Outcome.ABORT; public abstract class AbstractSetterInterceptor extends AbstractExchangeExpressionInterceptor { @@ -50,19 +51,21 @@ private Outcome handleInternal(Exchange exchange, Flow flow) { try { setValue(exchange, flow, exchangeExpression.evaluate(exchange, flow, getExpressionReturnType())); } catch (Exception e) { - log.error("While evaluating expression {} for field {}: {}", expression, fieldName,e.getMessage()); + var root = ExceptionUtil.getRootCause(e); + var message = "While evaluating expression %s for field %s: %s".formatted(expression, fieldName, root.getMessage()); + log.info(message); + if (failOnError) { - internal(getRouter().isProduction(),getDisplayName()) + internal(getRouter().isProduction(), getDisplayName()) .title("Error evaluating expression!") .internal("field", fieldName) .internal("value", expression) - .exception(e) + .exception(root) .stacktrace(false) .buildAndSetResponse(exchange); return ABORT; - } else { - log.info("Error evaluating {} but 'FailOnError' is false therefore ignoring. Exception :{}", expression,e); } + log.info("Error evaluating {} but 'FailOnError' is false therefore ignoring: {}", expression, message); } return CONTINUE; } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java new file mode 100644 index 0000000000..9d07f2c267 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java @@ -0,0 +1,103 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.lang; + +import com.predic8.membrane.annot.*; + +import javax.xml.namespace.*; +import java.util.*; + +import static javax.xml.XMLConstants.NULL_NS_URI; + +@MCElement(name="namespaces", topLevel = true) +public class Namespaces { + + //NamespaceContext nsContext; + private List namespaces; + private NamespaceContextImpl nsContext = new NamespaceContextImpl(); + + public NamespaceContext getNamespaceContext() { + return nsContext; + } + + /** + * @description Defines a regex and a replacement for the rewriting of the URI. + */ + @MCChildElement(allowForeign = false) + public void setNamespace(List namespace) { + this.namespaces = namespace; + } + + public List getNamespace() { + return namespaces; + } + + @MCElement(name = "namespace", topLevel = false, id = "xml-namespace") + public static class Namespace { + + public String prefix; + public String uri; + + public Namespace() { + } + + public String getPrefix() { + return prefix; + } + + @MCAttribute + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getUri() { + return uri; + } + + @MCAttribute + public void setUri(String uri) { + this.uri = uri; + } + } + + class NamespaceContextImpl implements NamespaceContext { + + @Override + public String getNamespaceURI(String prefix) { + return namespaces.stream() + .filter(ns -> prefix.equals(ns.prefix)) + .findFirst() + .map(ns -> ns.uri) + .orElse(NULL_NS_URI); + } + + @Override + public String getPrefix(String namespaceURI) { + return namespaces.stream() + .filter(ns -> namespaceURI.equals(ns.uri)) + .map(ns -> ns.prefix) + .findFirst() + .orElse(null); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return namespaces.stream() + .filter(ns -> namespaceURI.equals(ns.uri)) + .map(ns -> ns.prefix) + .iterator(); + } + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java index c89b52315c..46335c1c85 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java @@ -93,7 +93,7 @@ protected ExchangeExpression getExchangeExpression() { // If there is no expression use the client IP if (expression.isEmpty()) return null; - return ExchangeExpression.newInstance(router, language, expression); + return ExchangeExpression.newInstance(this, language, expression); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/soap/SampleSoapServiceInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/soap/SampleSoapServiceInterceptor.java index 8231c2e4b8..49f6dd1385 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/soap/SampleSoapServiceInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/soap/SampleSoapServiceInterceptor.java @@ -34,7 +34,7 @@ import static com.predic8.membrane.core.http.Response.*; import static com.predic8.membrane.core.interceptor.Outcome.*; import static com.predic8.membrane.core.openapi.util.Utils.*; -import static com.predic8.membrane.core.util.XMLUtil.*; +import static com.predic8.membrane.core.util.xml.XMLUtil.*; import static java.util.Objects.*; import static javax.xml.stream.XMLStreamConstants.*; diff --git a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java index e6f1651c62..f451522e44 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java @@ -17,6 +17,7 @@ import com.predic8.membrane.core.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.groovy.*; import com.predic8.membrane.core.lang.jsonpath.*; import com.predic8.membrane.core.lang.spel.*; @@ -48,12 +49,46 @@ enum Language {GROOVY, SPEL, XPATH, JSONPATH} */ T evaluate(Exchange exchange, Interceptor.Flow flow, Class type) throws ExchangeExpressionException; - static ExchangeExpression newInstance(Router router, Language language, String expression) { + /** + * Clients of this class should pass an interceptor if possible. Otherwise use the InterceptorAdapter to wrap it. + * There is no convenience method on purpose to make the clients pass the interceptor. From the interceptor you can always get the router. + * @param interceptor + * @param language + * @param expression + * @return + */ + static ExchangeExpression newInstance(Interceptor interceptor, Language language, String expression) { return switch (language) { - case GROOVY -> new GroovyExchangeExpression(router, expression); + case GROOVY -> new GroovyExchangeExpression(interceptor, expression); case SPEL -> new SpELExchangeExpression(expression,null); - case XPATH -> new XPathExchangeExpression(expression); + case XPATH -> new XPathExchangeExpression(interceptor,expression); case JSONPATH -> new JsonpathExchangeExpression(expression); }; } + /** + * Allows to pass an Interceptor as an argument where there is no interceptor e.g. Target + */ + class InterceptorAdapter extends AbstractInterceptor implements XMLNamespaceSupport{ + + private Namespaces namespaces; + + public InterceptorAdapter(Router router) { + this.router = router; + } + + public InterceptorAdapter(Router router, Namespaces namespaces) { + this.router = router; + this.namespaces = namespaces; + } + + @Override + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + @Override + public Namespaces getNamespaces() { + return namespaces; + } + } } diff --git a/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java index 87d5d521d2..a2d94deecb 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java @@ -15,6 +15,7 @@ import com.predic8.membrane.core.*; import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.Interceptor.*; import com.predic8.membrane.core.lang.spel.*; @@ -36,17 +37,17 @@ public class TemplateExchangeExpression extends AbstractExchangeExpression { private final List tokens; - public static ExchangeExpression newInstance(Router router, Language language, String expression) { + public static ExchangeExpression newInstance(Interceptor interceptor, Language language, String expression) { // SpEL comes with its own templating if (language == SPEL) { return new SpELExchangeExpression(expression, new SpELExchangeExpression.DollarBracketTemplateParserContext()); } - return new TemplateExchangeExpression(router, language, expression); + return new TemplateExchangeExpression(interceptor, language, expression); } - protected TemplateExchangeExpression(Router router, Language language, String expression) { + protected TemplateExchangeExpression(Interceptor interceptor, Language language, String expression) { super(expression); - tokens = parseTokens(router,language, expression); + tokens = parseTokens(interceptor,language, expression); } @Override @@ -83,7 +84,7 @@ private String evaluateToString(Exchange exchange, Flow flow) { return line.toString(); } - protected static List parseTokens(Router router, Language language, String expression) { + protected static List parseTokens(Interceptor interceptor, Language language, String expression) { log.debug("Parsing: {}",expression); List tokens = new ArrayList<>(); @@ -95,7 +96,7 @@ protected static List parseTokens(Router router, Language language, Strin } String expr = m.group(3); if (expr != null) { - tokens.add(new Expression(ExchangeExpression.newInstance(router, language, expr))); + tokens.add(new Expression(ExchangeExpression.newInstance(interceptor, language, expr))); } } log.debug("Tokens: {}", tokens); diff --git a/core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpression.java index 58bb78b38f..feab6d92f1 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/groovy/GroovyExchangeExpression.java @@ -35,9 +35,9 @@ public class GroovyExchangeExpression extends AbstractExchangeExpression { private final Function, Object> script; private final Router router; - public GroovyExchangeExpression(Router router, String source) { + public GroovyExchangeExpression(Interceptor interceptor, String source) { super(source); - this.router = router; + this.router = interceptor.getRouter(); try { script = new GroovyLanguageSupport().compileScript(router.getBackgroundInitializer(), null, source); } catch (MultipleCompilationErrorsException e) { diff --git a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java index 0247bff97a..6c74dc8189 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java @@ -17,25 +17,37 @@ import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.util.xml.*; +import com.predic8.membrane.core.util.xml.parser.*; import org.slf4j.*; import org.w3c.dom.*; import javax.xml.namespace.*; import javax.xml.xpath.*; -import static com.predic8.membrane.core.util.XMLUtil.*; import static javax.xml.xpath.XPathConstants.*; public class XPathExchangeExpression extends AbstractExchangeExpression { private static final Logger log = LoggerFactory.getLogger(XPathExchangeExpression.class.getName()); + private static XmlParser parser; + + private Namespaces namespaces; + + // Let all expressions share the same XPathFactory. private static final XPathFactory factory = XPathFactory.newInstance(); - public XPathExchangeExpression(String xpath) { + public XPathExchangeExpression(Interceptor interceptor, String xpath) { super(xpath); + + if (interceptor instanceof XMLNamespaceSupport xns) { + namespaces = xns.getNamespaces(); + } + + parser = HardenedXmlParser.getInstance(); } @Override @@ -72,7 +84,18 @@ private Object evalutateAndCast(Message msg, QName xmlType) throws XPathExpressi log.debug("Evaluating: {}", expression); log.debug("Body: {}", msg.getBodyAsStringDecoded()); // is expensive! } - // XPath is not thread safe! Therefore every time the factory is called! - return factory.newXPath().evaluate(expression, getInputSource(msg), xmlType); + + // XPath is not thread safe! Therefore, every time the factory is called! + XPath xPath = factory.newXPath(); + + if (namespaces != null) { + xPath.setNamespaceContext(namespaces.getNamespaceContext()); + } + + return xPath.evaluate(expression, parser.parse(XMLUtil.getInputSource(msg)), xmlType); + } + + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; } } diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java index c357baf6a2..f38d9c834e 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java @@ -20,9 +20,9 @@ import com.predic8.membrane.annot.MCChildElement; import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.annot.MCTextContent; -import com.predic8.membrane.core.interceptor.lang.Polyglot; -import com.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +import com.predic8.membrane.core.interceptor.lang.*; +import com.predic8.membrane.core.lang.*; +import com.predic8.membrane.core.lang.ExchangeExpression.*; import com.predic8.membrane.core.openapi.util.UriUtil; import com.predic8.membrane.core.proxies.ServiceProxy; import com.predic8.membrane.core.util.ConfigurationException; @@ -57,6 +57,7 @@ public class APIProxy extends ServiceProxy implements Polyglot { private String test; private String id; private ApiDescription description; + private Namespaces namespaces; protected Map apiRecords = new LinkedHashMap<>(); @@ -82,7 +83,7 @@ public void setSpecs(List specs) { public void init() { super.init(); if (test != null && !test.isEmpty()) { - exchangeExpression = ExchangeExpression.newInstance(router, language, test); + exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, test); } key = new APIProxyKey(key, exchangeExpression, !specs.isEmpty()); initOpenAPI(); @@ -235,4 +236,17 @@ public String getContent() { return content; } } + + /** + * Declaration of XML namespaces for XPath expressions. + * @param namespaces + */ + @MCChildElement(allowForeign = true, order = 10) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java index 858f800111..8d498517aa 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java @@ -19,9 +19,12 @@ import com.predic8.membrane.core.config.*; import com.predic8.membrane.core.config.security.*; import com.predic8.membrane.core.exchange.Exchange; -import com.predic8.membrane.core.interceptor.Interceptor; +import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression; +import com.predic8.membrane.core.lang.ExchangeExpression.*; import com.predic8.membrane.core.lang.TemplateExchangeExpression; +import com.predic8.membrane.core.lang.xpath.*; import com.predic8.membrane.core.transport.ssl.*; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; @@ -96,7 +99,7 @@ public void setPath(Path path) { *

*/ @MCElement(name = "target", topLevel = false) - public static class Target { + public static class Target implements XMLNamespaceSupport { private String host; private int port = -1; private String method; @@ -107,8 +110,13 @@ public static class Target { private SSLParser sslParser; + protected Namespaces namespaces; + public void init(Router router) { - if (url != null) exchangeExpression = TemplateExchangeExpression.newInstance(router, language, url); + if (url != null) { + exchangeExpression = TemplateExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, url); + } + } public String compileUrl(Exchange exc, Interceptor.Flow flow) { @@ -228,6 +236,15 @@ public ExchangeExpression.Language getLanguage() { public void setLanguage(ExchangeExpression.Language language) { this.language = language; } + + @MCChildElement(allowForeign = true, order = 100) + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } } protected Target target = new Target(); diff --git a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java index 71dd4b53b6..6b2584542b 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java @@ -13,6 +13,10 @@ limitations under the License. */ package com.predic8.membrane.core.util; +import com.predic8.membrane.core.util.xml.parser.*; + +import java.util.*; + public class ExceptionUtil { public static String concatMessageAndCauseMessages(Throwable throwable) { StringBuilder sb = new StringBuilder(); @@ -25,4 +29,12 @@ public static String concatMessageAndCauseMessages(Throwable throwable) { } while (throwable != null); return sb.toString(); } + + public static Throwable getRootCause(Throwable t) { + Throwable cause = t; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause; + } } diff --git a/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java b/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java index 6c36a5bf77..09a33dc352 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java @@ -29,7 +29,7 @@ import static com.predic8.membrane.core.Constants.*; import static com.predic8.membrane.core.http.MimeType.*; -import static com.predic8.membrane.core.util.XMLUtil.*; +import static com.predic8.membrane.core.util.xml.XMLUtil.*; import static java.nio.charset.StandardCharsets.*; import static javax.xml.stream.XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES; import static javax.xml.stream.XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES; diff --git a/core/src/main/java/com/predic8/membrane/core/util/XMLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/xml/XMLUtil.java similarity index 98% rename from core/src/main/java/com/predic8/membrane/core/util/XMLUtil.java rename to core/src/main/java/com/predic8/membrane/core/util/xml/XMLUtil.java index d321fd6d8e..d1023b4896 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/XMLUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/XMLUtil.java @@ -11,7 +11,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package com.predic8.membrane.core.util; +package com.predic8.membrane.core.util.xml; import com.predic8.membrane.core.http.*; import org.jetbrains.annotations.*; diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java new file mode 100644 index 0000000000..2477c765d7 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -0,0 +1,89 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.util.xml.parser; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import javax.xml.parsers.*; +import java.io.StringReader; + +import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING; + +/** + * Thread-safe, XXE-hardened XML parser implementation. + * A fresh {@link DocumentBuilder} is created for each call. + * Instances are immutable and can safely be shared across threads. + */ +public final class HardenedXmlParser implements XmlParser { + + private final DocumentBuilderFactory factory = createFactory(true); + + private static XmlParser INSTANCE; + + private HardenedXmlParser() {} + + public static XmlParser getInstance() { + if (INSTANCE == null) { + INSTANCE = new HardenedXmlParser(); + return INSTANCE; + } + + return INSTANCE; + } + + private static DocumentBuilderFactory createFactory(boolean namespaceAware) { + DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); + f.setNamespaceAware(namespaceAware); + + try { + // XXE protection and secure defaults + f.setFeature(FEATURE_SECURE_PROCESSING, true); + f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + f.setFeature("http://xml.org/sax/features/external-general-entities", false); + f.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + } catch (ParserConfigurationException e) { + throw new IllegalStateException("Secure XML parser features not supported", e); + } + + try { f.setXIncludeAware(false); } catch (UnsupportedOperationException ignore) {} + f.setExpandEntityReferences(false); + return f; + } + + private DocumentBuilder newBuilder() { + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + // Prevent any external entity resolution + builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); + return builder; + } catch (ParserConfigurationException e) { + throw new XmlParseException("Failed to create XML parser: " + e.getMessage(), e); + } + } + + @Override + public Document parse(InputSource source) { + if (source == null) + throw new IllegalArgumentException("source must not be null"); + + try { + return newBuilder().parse(source); + } catch (Exception e) { + throw new XmlParseException("Could not parse XML document: " + e.getMessage()); // No stacktrace needed + } + } +} + diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParseException.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParseException.java new file mode 100644 index 0000000000..e8dec533c8 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParseException.java @@ -0,0 +1,34 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.util.xml.parser; + + +/** + * Unchecked exception thrown when XML parsing fails. + * This avoids forcing callers to handle {@link org.xml.sax.SAXException} + * or {@link java.io.IOException} explicitly. + */ +public class XmlParseException extends RuntimeException { + + public XmlParseException(String message) { + super(message); + } + + public XmlParseException(String message, Throwable cause) { + super(message, cause); + } + +} + diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParser.java new file mode 100644 index 0000000000..d78d826715 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/XmlParser.java @@ -0,0 +1,34 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.util.xml.parser; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +/** + * Strategy interface for XML parsing. + * Implementations are expected to be thread-safe and XXE-hardened. + */ +public interface XmlParser { + + /** + * Parses the given XML input source into a DOM document. + * + * @param source the XML input source to parse + * @return a DOM {@link Document} + * @throws XmlParseException if parsing fails + */ + Document parse(InputSource source) throws XmlParseException; +} diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java new file mode 100644 index 0000000000..88d6c78d41 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java @@ -0,0 +1,42 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.lang; + +import com.predic8.membrane.core.util.*; +import org.junit.jupiter.api.*; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class NamespacesTest { + + Namespaces namespaces = new Namespaces(); + + @BeforeEach + void setup() { + Namespaces.Namespace ns = new Namespaces.Namespace(); + ns.prefix = "p8"; + ns.uri = "https://predic8.de"; + namespaces.setNamespace(List.of(ns)); + } + + @Test + void namespaceContext() { + assertEquals("https://predic8.de", namespaces.getNamespaceContext().getNamespaceURI("p8")); + assertEquals("p8", namespaces.getNamespaceContext().getPrefix("https://predic8.de")); + assertEquals("p8", CollectionsUtil.toList(namespaces.getNamespaceContext().getPrefixes("https://predic8.de")).getFirst()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java index a771ac995c..bec1dc058f 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java @@ -65,17 +65,17 @@ static void tearDown() { protected abstract Language getLanguage(); protected Object evalObject(String expression) { - return ExchangeExpression.newInstance(router, getLanguage(),expression) + return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) .evaluate(exchange,flow, Object.class); } protected boolean evalBool(String expression) { - return ExchangeExpression.newInstance(router, getLanguage(),expression) + return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) .evaluate(exchange,flow, Boolean.class); } protected String evalString(String expression) { - return ExchangeExpression.newInstance(router, getLanguage(),expression) + return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) .evaluate(exchange,flow, String.class); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/TemplateExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/TemplateExchangeExpressionTest.java index fd2cf94b52..886472d2d1 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/TemplateExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/TemplateExchangeExpressionTest.java @@ -42,7 +42,7 @@ static void setUp() throws Exception { @Test void text() { - assertIterableEquals(List.of(new Text("aaa")), parseTokens(router,language,"aaa")); + assertIterableEquals(List.of(new Text("aaa")), parseTokens(new InterceptorAdapter(router),language,"aaa")); } @Test @@ -66,6 +66,6 @@ void multiple() { } private static String eval(String expr) { - return new TemplateExchangeExpression(router, language, expr).evaluate(exc, REQUEST,String.class); + return new TemplateExchangeExpression(new InterceptorAdapter(router), language, expr).evaluate(exc, REQUEST,String.class); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java index 204784747f..abe4c9d1f0 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java @@ -116,12 +116,12 @@ void emptyBodyForBoolean() throws URISyntaxException { @Test void wrongContentType() throws URISyntaxException { - assertEquals("", ExchangeExpression.newInstance(router, JSONPATH, "$") + assertEquals("", ExchangeExpression.newInstance(new InterceptorAdapter(router), JSONPATH, "$") .evaluate(Request.post("/foo").contentType(TEXT_XML).buildExchange(), REQUEST, String.class)); } private static T evaluateWithEmptyBodyFor(Class type) throws URISyntaxException { - return ExchangeExpression.newInstance(router, JSONPATH, "$") + return ExchangeExpression.newInstance(new InterceptorAdapter(router), JSONPATH, "$") .evaluate(get("/foo").buildExchange(), REQUEST, type); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java index 1b26eeb77b..4cd6ed1732 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java @@ -14,6 +14,9 @@ package com.predic8.membrane.core.lang.xpath; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.http.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; import org.junit.jupiter.api.*; @@ -23,7 +26,9 @@ import static com.predic8.membrane.core.http.MimeType.*; import static com.predic8.membrane.core.http.Request.*; +import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; +import static com.predic8.membrane.core.lang.ExchangeExpression.newInstance; import static org.junit.jupiter.api.Assertions.*; class XPathExchangeExpressionTest extends AbstractExchangeExpressionTest { @@ -110,4 +115,25 @@ void wrongContentType() { assertEquals("John Doe",evalString("/persons/name[1]")); } + @Nested + class Namespaces { + + Exchange pExc; + + @BeforeEach + void setup() throws URISyntaxException { + pExc = Request.post("/person").xml(""" + + Trevor + + """).buildExchange(); + } + + @Test + void localName() { + assertEquals("Trevor", newInstance( new InterceptorAdapter(router),getLanguage(), + "//*[local-name()='firstname']") + .evaluate(pExc, REQUEST, String.class)); + } + } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/XMLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/XMLUtilTest.java index f87ea6c06d..03a45102f9 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/XMLUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/XMLUtilTest.java @@ -13,6 +13,7 @@ limitations under the License. */ package com.predic8.membrane.core.util; +import com.predic8.membrane.core.util.xml.*; import org.junit.jupiter.api.*; import javax.xml.namespace.*; diff --git a/distribution/examples/templating/json/README.md b/distribution/examples/templating/json/README.md index 95f8f9a2ad..90bf35fd30 100644 --- a/distribution/examples/templating/json/README.md +++ b/distribution/examples/templating/json/README.md @@ -12,9 +12,9 @@ Execute the following steps: 2. Have a look at `proxies.xml`. -2. Open a commandline and execute `membrane.sh` or `membrane.cmd` +3. Open a commandline and execute `membrane.sh` or `membrane.cmd` -3. Run this command from a second commandline: +4. Run this command from a second commandline: ```bash curl "http://localhost:2000/json?answer=42" @@ -26,7 +26,7 @@ Execute the following steps: { "answer": 42 } ``` -4. Then execute: +5. Then execute: ```bash curl -d '{"city":"Berlin"}' -H "Content-Type: application/json" "http://localhost:2000" diff --git a/distribution/examples/xml/namespaces/README.md b/distribution/examples/xml/namespaces/README.md new file mode 100644 index 0000000000..eae89885bd --- /dev/null +++ b/distribution/examples/xml/namespaces/README.md @@ -0,0 +1,16 @@ +# XML Namespaces Example + +How to declare and use namespaces with setProperty, if, ... + +## Running the Sample +***Note:*** *The requests are also available in the requests.http file.* + +1. **Navigate** to the `examples/xml/namespaces` directory. +2. **Start** the API Gateway by executing `membrane.sh` (Linux/Mac) or `membrane.cmd` (Windows). +3. **Run**: + - Send: + ``` + curl -d @person.xml localhost:2000 + ``` +4. **Understand**: + Take a look at the `proxies.xml` file. diff --git a/distribution/examples/xml/namespaces/membrane.cmd b/distribution/examples/xml/namespaces/membrane.cmd new file mode 100644 index 0000000000..33f755ee0b --- /dev/null +++ b/distribution/examples/xml/namespaces/membrane.cmd @@ -0,0 +1,17 @@ +@echo off +setlocal +set "dir=%CD%" +:loop +if exist "%dir%\starter.jar" if exist "%dir%\scripts\run-membrane.cmd" goto found +for %%A in ("%dir%\..") do set "next=%%~fA" +if /I "%next%"=="%dir%" goto notfound +set "dir=%next%" +if "%dir%"=="%SystemDrive%\" goto notfound +goto loop +:found +set "ROOT=%dir%" +call "%ROOT%\scripts\run-membrane.cmd" %* +exit /b %ERRORLEVEL% +:notfound +>&2 echo Could not locate Membrane root. Ensure directory structure is correct. +exit /b 1 diff --git a/distribution/examples/xml/namespaces/membrane.sh b/distribution/examples/xml/namespaces/membrane.sh new file mode 100755 index 0000000000..96054f8c85 --- /dev/null +++ b/distribution/examples/xml/namespaces/membrane.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Default: ./proxies.xml (next to this script); fallback -> $MEMBRANE_HOME/conf/proxies.xml +# JAVA_OPTS: relative -D paths are auto-resolved against $MEMBRANE_HOME (absolute/URI unchanged). +# Examples: +# export JAVA_OPTS='-Dlog4j.configurationFile=examples/logging/access/log4j2_access.xml' +# export JAVA_OPTS='-Dlog4j.configurationFile=/abs/path/log4j2.xml' + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +dir="$SCRIPT_DIR" +while [ "$dir" != "/" ]; do + if [ -f "$dir/starter.jar" ] && [ -f "$dir/scripts/run-membrane.sh" ]; then + export MEMBRANE_HOME="$dir" + export MEMBRANE_CALLER_DIR="$SCRIPT_DIR" + exec sh "$dir/scripts/run-membrane.sh" "$@" + fi + dir=$(dirname "$dir") +done + +echo "Could not locate Membrane root. Ensure directory structure is correct." >&2 +exit 1 \ No newline at end of file diff --git a/distribution/examples/xml/namespaces/person.xml b/distribution/examples/xml/namespaces/person.xml new file mode 100644 index 0000000000..5365e065d8 --- /dev/null +++ b/distribution/examples/xml/namespaces/person.xml @@ -0,0 +1,6 @@ + + Hans + + Cologne + + \ No newline at end of file diff --git a/distribution/examples/xml/namespaces/proxies.xml b/distribution/examples/xml/namespaces/proxies.xml new file mode 100644 index 0000000000..059d75cb83 --- /dev/null +++ b/distribution/examples/xml/namespaces/proxies.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + /person + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/examples/xml/namespaces/requests.http b/distribution/examples/xml/namespaces/requests.http new file mode 100644 index 0000000000..e270a2349f --- /dev/null +++ b/distribution/examples/xml/namespaces/requests.http @@ -0,0 +1,9 @@ +POST http://localhost:2000 +Content-Type: text/xml + + + Hans + + Cologne + + \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java index 5116e0abaa..616816771e 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java @@ -14,11 +14,13 @@ package com.predic8.membrane.examples.withinternet.test; import com.predic8.membrane.examples.util.*; +import io.restassured.filter.log.*; import org.junit.jupiter.api.*; import java.io.*; import static io.restassured.RestAssured.*; +import static io.restassured.filter.log.LogDetail.ALL; import static org.hamcrest.Matchers.*; public class VersioningSoapXsltExampleTest extends DistributionExtractingTestcase { @@ -53,6 +55,7 @@ public void test() throws Exception { .body(request_old) .post("http://localhost:2000/city-service") .then() + .log().ifValidationFails(ALL) .statusCode(200) .body("Envelope.Body.getCityResponse.country", equalTo("Germany")); // @formatter:on diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java new file mode 100644 index 0000000000..2bef1bc6c4 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -0,0 +1,31 @@ +/* Copyright 2024 predic8 GmbH, www.predic8.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ +package com.predic8.membrane.examples.withoutinternet.test.xml.namespaces; + +import com.predic8.membrane.examples.util.*; +import org.junit.jupiter.api.*; + +import static io.restassured.RestAssured.*; + +public class NamespacesExampleTest extends AbstractSampleMembraneStartStopTestcase { + + @Override + protected String getExampleDirName() { + return "xml/namespaces"; + } + + // TODO Implement + + +} diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index bc4b0a1dce..48bb4e5647 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -31,6 +31,7 @@ # 6.4.0 +- SessionManagerTest: refactor, too slow for Unittest. Move to integration tests. - Refactor: Cookie maybe centralize Cookie Handling in a Cookie class - Loadbalancing description with pacemaker - JSONBody @@ -45,6 +46,7 @@ - public abstract void init() throws Exception; - getEndSessionEndpoint() throws Exception - doDynamicRegistration(List callbackURLs) throws Exception + ## Release Notes: - JSON Schema validation support for JSON Schema 2019-09 and 2020-12 (via networknt json-schema-validator). From 7b0b78b88ebadd4d89b48315806e4b59a0e70fb3 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 1 Nov 2025 17:07:26 +0100 Subject: [PATCH 02/16] refactor: minor --- .../idempotency/IdempotencyInterceptor.java | 24 +- .../lang/AbstractLanguageInterceptor.java | 2 - .../core/interceptor/lang/Namespaces.java | 2 +- .../core/proxies/AbstractServiceProxy.java | 2 +- .../predic8/membrane/core/util/WSDLUtil.java | 2 +- .../util/xml/parser/HardenedXmlParser.java | 6 +- distribution/conf/apis.yaml | 509 ++++++++++++++++++ distribution/conf/log4j2.xml | 2 +- distribution/conf/simple.yaml | 27 + .../test/VersioningSoapXsltExampleTest.java | 3 +- .../xml/namespaces/NamespacesExampleTest.java | 3 - 11 files changed, 553 insertions(+), 29 deletions(-) create mode 100644 distribution/conf/apis.yaml create mode 100644 distribution/conf/simple.yaml diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java index d86962230f..7c1026216c 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java @@ -14,25 +14,19 @@ package com.predic8.membrane.core.interceptor.idempotency; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -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.AbstractInterceptor; -import com.predic8.membrane.core.interceptor.Outcome; +import com.google.common.cache.*; +import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.lang.*; -import com.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +import com.predic8.membrane.core.lang.*; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; -import static com.predic8.membrane.core.exceptions.ProblemDetails.user; -import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; +import static com.predic8.membrane.core.exceptions.ProblemDetails.*; +import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; +import static com.predic8.membrane.core.interceptor.Outcome.*; import static com.predic8.membrane.core.interceptor.Outcome.ABORT; -import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; -import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; /** * @description

Prevents duplicate request processing based on a dynamic idempotency key.

diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java index 39df993242..dd3f1c4172 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java @@ -24,8 +24,6 @@ public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLNamespaceSupport { - private static final Logger log = LoggerFactory.getLogger(AbstractLanguageInterceptor.class); - /** * SpEL is default */ diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java index 9d07f2c267..baebe54f0c 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java @@ -26,7 +26,7 @@ public class Namespaces { //NamespaceContext nsContext; private List namespaces; - private NamespaceContextImpl nsContext = new NamespaceContextImpl(); + private final NamespaceContextImpl nsContext = new NamespaceContextImpl(); public NamespaceContext getNamespaceContext() { return nsContext; diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java index 8d498517aa..b26eedcce8 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java @@ -120,7 +120,7 @@ public void init(Router router) { } public String compileUrl(Exchange exc, Interceptor.Flow flow) { - /** + /* * Will always evaluate on every call. This is fine as SpEL is fast enough and performs its own optimizations. * 1.000.000 calls ~10ms */ diff --git a/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java index b824d2adbd..e8c8e658c6 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java @@ -24,7 +24,7 @@ import static com.predic8.membrane.core.Constants.WSDL_SOAP11_NS; import static com.predic8.membrane.core.Constants.WSDL_SOAP12_NS; import static com.predic8.membrane.core.util.WSDLUtil.Direction.*; -import static com.predic8.membrane.core.util.XMLUtil.groovyToJavaxQName; +import static com.predic8.membrane.core.util.xml.XMLUtil.groovyToJavaxQName; public class WSDLUtil { diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java index 2477c765d7..7c56237656 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -29,7 +29,7 @@ */ public final class HardenedXmlParser implements XmlParser { - private final DocumentBuilderFactory factory = createFactory(true); + private final DocumentBuilderFactory factory = createFactory(); private static XmlParser INSTANCE; @@ -44,9 +44,9 @@ public static XmlParser getInstance() { return INSTANCE; } - private static DocumentBuilderFactory createFactory(boolean namespaceAware) { + private static DocumentBuilderFactory createFactory() { DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); - f.setNamespaceAware(namespaceAware); + f.setNamespaceAware(true); try { // XXE protection and secure defaults diff --git a/distribution/conf/apis.yaml b/distribution/conf/apis.yaml new file mode 100644 index 0000000000..7113e23a84 --- /dev/null +++ b/distribution/conf/apis.yaml @@ -0,0 +1,509 @@ +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +kind: api +spec: + port: 2000 + target: + url: https://api.predic8.de + +--- + +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +kind: api +spec: + port: 2001 + path: + uri: /fruit/{id} + target: + url: https://api.predic8.de/shop/v2/products/${pathParam.id} + +--- + +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2002 + specs: + - openapi: + location: "fruitshop-api.yml" + validateRequests: yes + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2003 + flow: + - log: + message: "Headers: ${header}" + target: + url: https://api.predic8.de + +--- + +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2004 + path: + uri: /shop + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2005 + specs: + - openapi: + location: "fruitshop-api.yml" + validateRequests: yes + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2007 + method: POST + flow: + - response: + - static: + src: | + POST is blocked! + - return: + statusCode: 405 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2007 + path: + uri: /shop/v2/products/.* + isRegExp: true + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2007 + path: + uri: /shop + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2007 + host: www.predic8.de + flow: + - response: + - static: + src: Homepage + - return: + statusCode: 200 + path: + uri: /shop + target: + url: https://api.predic8.de + + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2008 + test: params.city == 'Paris' + flow: + - response: + - static: + src: Oui! + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2009 + path: + uri: /health + flow: + - response: + - static: + src: I'm good. + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2010 + path: + uri: /nothing + flow: + - response: + - static: + src: "Nothing to see here." + - return: + statusCode: 404 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2011 + flow: + - rewriter: + - map: + from: ^/fruitshop/(.*) + to: /shop/v2/$1 + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2012 + flow: + - groovy: + src: | + println "I'm executed in the ${flow} flow" + println "HTTP Headers:\n${header}" + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2013 + flow: + - groovy: + src: | + sites = ["https://api.predic8.de","https://membrane-api.io","https://predic8.de"] + Collections.shuffle sites + exchange.destinations = sites + target: {} + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2014 + flow: + - groovy: + src: | + Response.ok() + .contentType("application/json") + .header("X-Foo", "bar") + .body(""" + { + "success": true + }""") + .build() + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2015 + flow: + - javascript: + src: | + console.log("------------ Headers: -------------"); + var fields = header.getAllHeaderFields(); + for (var i = 0; i < fields.length; i++) { + console.log(fields[i]); + } + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2016 + flow: + - javascript: + src: | + var body = JSON.stringify({ + foo: 7, + bar: 42 + }); + Response.ok(body).contentType("application/json").build(); + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2017 + flow: + - response: + - setHeader: + name: Access-Control-Allow-Origin + value: "*" + - setHeader: + name: Access-Control-Allow-Methods + value: GET + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2018 + flow: + - response: + - setHeader: + name: X-Product-Id + value: ${jsonPath('$.id')} + - setHeader: + name: X-Product-Name + value: ${$.name} + language: jsonpath + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2019 + flow: + - response: + - headerFilter: + rules: + - include: + pattern: "X-XSS-Protection" + - exclude: + pattern: "X-.*" + target: + url: https://www.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2020 + flow: + - request: + - template: + contentType: application/json + pretty: true + src: | + { "answer": ${params.answer} } + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2021 + flow: + - request: + - template: + contentType: text/plain + src: | + City: ${json.city} + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2022 + flow: + - request: + - template: + contentType: application/json + src: | + { + "destination": "${json.city}" + } + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2023 + flow: + - request: + - template: + contentType: application/xml + src: | + + ${json.city} + + - return: + statusCode: 200 + +--- +spec: + port: 2024 + flow: + - request: + - javascript: + src: | + function convertDate(d) { + return d.getFullYear() + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" + ("0"+d.getDate()).slice(-2); + } + + ({ + id: json.id, + date: convertDate(new Date(json.date)), + client: json.customer, + total: json.items.map(i => i.quantity * i.price).reduce((a,b) => a+b), + positions: json.items.map(i => ({ + pieces: i.quantity, + price: i.price, + article: i.description + })) + }) + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2025 + flow: + - response: + - beautifier: {} # Response flow is reversed + - template: + contentType: application/xml + src: | + Baz + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2026 + flow: + - response: + - if: + test: statusCode matches '4\d\d' + flow: + - static: + src: "Stupid User!" + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2027 + flow: + - apiKey: + stores: + - keys: + - secret: + value: abc123 + - secret: + value: secret123 + - return: + statusCode: 200 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2028 + flow: + - jwtAuth: + expectedAud: "api://2axxxx16-xxxx-xxxx-xxxx-faxxxxxxxxf0" + jwks: + jwksUris: https://login.microsoftonline.com/common/discovery/keys + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2029 + flow: + - oauth2Resource2: + membrane: + src: https://accounts.google.com + clientId: INSERT_CLIENT_ID + clientSecret: INSERT_CLIENT_SECRET + scope: email profile + subject: sub + - groovy: + src: | + // Get email from OAuth2 and forward it to the backend + def oauth2 = exc.properties.'membrane.oauth2' + header.setValue('X-EMAIL',oauth2.userinfo.email) + target: + url: https://backend + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +#spec: +# port: 2030 +# flow: +# - oauth2authserver: +# location: logindialog +# issuer: http://localhost:2000 +# consentFile: consent.json +# staticUserDataProvider: +# users: +# - user: +# username: john +# password: secret +# email: john@predic8.de +# bearerJwtToken: {} +# claims: +# scopes: +# - scope: +# id: username +# target: +# url: http://backend + + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2031 + flow: + - basicAuthentication: + users: + - user: + username: bob + password: secret + - user: + username: alice + password: secret + target: + host: localhost + port: 8080 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2032 + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 443 + ssl: + keystore: + location: membrane.p12 + password: secret + keyPassword: secret + target: + host: localhost + port: 8080 + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2033 + flow: + - xmlProtection: + maxElementNameLength: 100 + target: + url: https://api.predic8.de + +--- +# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json +spec: + port: 2034 + flow: + - jsonProtection: + maxDepth: 5 + maxKeyLength: 30 + maxStringLength: 100000 + target: + url: https://api.predic8.de \ No newline at end of file diff --git a/distribution/conf/log4j2.xml b/distribution/conf/log4j2.xml index 81af7c9bbd..d8b1a5e9f7 100644 --- a/distribution/conf/log4j2.xml +++ b/distribution/conf/log4j2.xml @@ -9,7 +9,7 @@ - + diff --git a/distribution/conf/simple.yaml b/distribution/conf/simple.yaml new file mode 100644 index 0000000000..fbf4b5c7aa --- /dev/null +++ b/distribution/conf/simple.yaml @@ -0,0 +1,27 @@ +openapi: 3.1.0 +info: + title: Simple Parameters API + version: "1.0.0" +servers: + - url: http://localhost + +paths: + /foo: + get: + parameters: + - name: bar + in: query + explode: true + schema: + type: array + items: + type: string + - name: baz + in: query + explode: true + schema: + type: integer + responses: + "200": + description: OK + diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java index 616816771e..1c5b8d9488 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/VersioningSoapXsltExampleTest.java @@ -14,13 +14,12 @@ package com.predic8.membrane.examples.withinternet.test; import com.predic8.membrane.examples.util.*; -import io.restassured.filter.log.*; import org.junit.jupiter.api.*; import java.io.*; import static io.restassured.RestAssured.*; -import static io.restassured.filter.log.LogDetail.ALL; +import static io.restassured.filter.log.LogDetail.*; import static org.hamcrest.Matchers.*; public class VersioningSoapXsltExampleTest extends DistributionExtractingTestcase { diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index 2bef1bc6c4..fe4ee4b54f 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -14,9 +14,6 @@ package com.predic8.membrane.examples.withoutinternet.test.xml.namespaces; import com.predic8.membrane.examples.util.*; -import org.junit.jupiter.api.*; - -import static io.restassured.RestAssured.*; public class NamespacesExampleTest extends AbstractSampleMembraneStartStopTestcase { From f0df650c67ccc27b3abbd8f302cb21ace186c2db Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 18:44:05 +0100 Subject: [PATCH 03/16] refactor: minor --- .../core/interceptor/lang/Namespaces.java | 1 - .../lang/xpath/XPathExchangeExpression.java | 4 +- .../membrane/core/util/ExceptionUtil.java | 3 ++ .../util/xml/parser/HardenedXmlParser.java | 6 ++- .../examples/xml/namespaces/README.md | 15 +++++- .../xml/namespaces/NamespacesExampleTest.java | 53 ++++++++++++++++++- 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java index baebe54f0c..a077c53022 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java @@ -24,7 +24,6 @@ @MCElement(name="namespaces", topLevel = true) public class Namespaces { - //NamespaceContext nsContext; private List namespaces; private final NamespaceContextImpl nsContext = new NamespaceContextImpl(); diff --git a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java index 6c74dc8189..15a76b95a1 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java @@ -33,7 +33,7 @@ public class XPathExchangeExpression extends AbstractExchangeExpression { private static final Logger log = LoggerFactory.getLogger(XPathExchangeExpression.class.getName()); - private static XmlParser parser; + private static XmlParser parser = HardenedXmlParser.getInstance(); private Namespaces namespaces; @@ -46,8 +46,6 @@ public XPathExchangeExpression(Interceptor interceptor, String xpath) { if (interceptor instanceof XMLNamespaceSupport xns) { namespaces = xns.getNamespaces(); } - - parser = HardenedXmlParser.getInstance(); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java index 6b2584542b..26c44510d1 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java @@ -18,6 +18,7 @@ import java.util.*; public class ExceptionUtil { + public static String concatMessageAndCauseMessages(Throwable throwable) { StringBuilder sb = new StringBuilder(); do { @@ -31,6 +32,8 @@ public static String concatMessageAndCauseMessages(Throwable throwable) { } public static Throwable getRootCause(Throwable t) { + if (t == null) + return null; Throwable cause = t; while (cause.getCause() != null && cause.getCause() != cause) { cause = cause.getCause(); diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java index 7c56237656..5785977e4c 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -31,7 +31,11 @@ public final class HardenedXmlParser implements XmlParser { private final DocumentBuilderFactory factory = createFactory(); - private static XmlParser INSTANCE; + /** + * Singleton instance. volatile is needed to against a reordering of instructions + * allowing another thread to see a partially constructed object + */ + private static volatile XmlParser INSTANCE; private HardenedXmlParser() {} diff --git a/distribution/examples/xml/namespaces/README.md b/distribution/examples/xml/namespaces/README.md index eae89885bd..c8b456b9f9 100644 --- a/distribution/examples/xml/namespaces/README.md +++ b/distribution/examples/xml/namespaces/README.md @@ -1,6 +1,17 @@ # XML Namespaces Example -How to declare and use namespaces with setProperty, if, ... +Namespaces are a powerful XML feature. Unfortunately, they can be a bit tricky to use. This example shows +how to declare and use namespaces. The namespace decarations can be used in: + +- setProperty +- setHeader +- if +- case +- target url +- api test +- call url + +The example shows how to declare and use them with XPath expressions. ## Running the Sample ***Note:*** *The requests are also available in the requests.http file.* @@ -9,7 +20,7 @@ How to declare and use namespaces with setProperty, if, ... 2. **Start** the API Gateway by executing `membrane.sh` (Linux/Mac) or `membrane.cmd` (Windows). 3. **Run**: - Send: - ``` + ```bash curl -d @person.xml localhost:2000 ``` 4. **Understand**: diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index fe4ee4b54f..d0c7f20a81 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -14,6 +14,12 @@ package com.predic8.membrane.examples.withoutinternet.test.xml.namespaces; import com.predic8.membrane.examples.util.*; +import org.junit.jupiter.api.*; + +import static com.predic8.membrane.core.http.MimeType.TEXT_XML; +import static io.restassured.RestAssured.given; +import static io.restassured.filter.log.LogDetail.ALL; +import static org.hamcrest.Matchers.containsString; public class NamespacesExampleTest extends AbstractSampleMembraneStartStopTestcase { @@ -22,7 +28,52 @@ protected String getExampleDirName() { return "xml/namespaces"; } - // TODO Implement + // TODO implement other cases as well + + @Test + void namespaceAwareXPathExtraction() throws Exception { + String xmlBody = """ + + Hans + + Cologne + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("Hans from Cologne")); + // @formatter:on + } + + @Test + void differentCity() throws Exception { + String xmlBody = """ + + Maria + + Berlin + + + """; + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("Maria from Berlin")); + // @formatter:on + } } From 6347d21f56ba7e45986b4609f6a99a8cc78d1ba4 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 18:50:07 +0100 Subject: [PATCH 04/16] refactor: minor --- .../core/interceptor/lang/AbstractLanguageInterceptor.java | 4 ++-- .../membrane/core/lang/xpath/XPathExchangeExpression.java | 2 +- .../test/xml/namespaces/NamespacesExampleTest.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java index dd3f1c4172..e8212141ee 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java @@ -30,8 +30,8 @@ public abstract class AbstractLanguageInterceptor extends AbstractInterceptor im protected Language language = SPEL; protected Namespaces namespaces; - public String getLanguage() { - return language.name(); + public Language getLanguage() { + return language; } /** diff --git a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java index 15a76b95a1..139ced5d93 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java @@ -33,7 +33,7 @@ public class XPathExchangeExpression extends AbstractExchangeExpression { private static final Logger log = LoggerFactory.getLogger(XPathExchangeExpression.class.getName()); - private static XmlParser parser = HardenedXmlParser.getInstance(); + private static final XmlParser parser = HardenedXmlParser.getInstance(); private Namespaces namespaces; diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index d0c7f20a81..9ce7c07f7b 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -31,7 +31,7 @@ protected String getExampleDirName() { // TODO implement other cases as well @Test - void namespaceAwareXPathExtraction() throws Exception { + void namespaceAwareXPathExtraction() { String xmlBody = """ Hans @@ -54,7 +54,7 @@ void namespaceAwareXPathExtraction() throws Exception { } @Test - void differentCity() throws Exception { + void differentCity() { String xmlBody = """ Maria From 74ddf93c6a49865a0637fbfb3ac1f70bf27d8e40 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 19:10:05 +0100 Subject: [PATCH 05/16] refactor: minor --- .../core/interceptor/XMLNamespaceSupport.java | 14 ++++++++++++++ .../lang/AbstractLanguageInterceptor.java | 6 ++---- .../predic8/membrane/core/util/ExceptionUtil.java | 4 ---- .../core/util/xml/parser/HardenedXmlParser.java | 9 ++++++--- distribution/examples/xml/namespaces/README.md | 4 ++-- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java index 27bc7780ad..b0910b164f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.core.interceptor; import com.predic8.membrane.core.interceptor.lang.*; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java index e8212141ee..50516a2dd5 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java @@ -16,11 +16,9 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.predic8.membrane.core.lang.ExchangeExpression.*; -import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; +import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLNamespaceSupport { diff --git a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java index 26c44510d1..aa1bcd3a72 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/ExceptionUtil.java @@ -13,10 +13,6 @@ limitations under the License. */ package com.predic8.membrane.core.util; -import com.predic8.membrane.core.util.xml.parser.*; - -import java.util.*; - public class ExceptionUtil { public static String concatMessageAndCauseMessages(Throwable throwable) { diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java index 5785977e4c..ca27f80be4 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -41,10 +41,13 @@ private HardenedXmlParser() {} public static XmlParser getInstance() { if (INSTANCE == null) { - INSTANCE = new HardenedXmlParser(); - return INSTANCE; + // Multiple threads can reach this point at the same time, therefore we need to synchronize + synchronized (HardenedXmlParser.class) { + if (INSTANCE == null) { + INSTANCE = new HardenedXmlParser(); + } + } } - return INSTANCE; } diff --git a/distribution/examples/xml/namespaces/README.md b/distribution/examples/xml/namespaces/README.md index c8b456b9f9..ee5c2e1395 100644 --- a/distribution/examples/xml/namespaces/README.md +++ b/distribution/examples/xml/namespaces/README.md @@ -1,7 +1,7 @@ # XML Namespaces Example -Namespaces are a powerful XML feature. Unfortunately, they can be a bit tricky to use. This example shows -how to declare and use namespaces. The namespace decarations can be used in: +Namespaces are a powerful XML feature. Unfortunately, they can be a bit tricky to use. This example shows +how to declare and use namespaces. The namespace declarations can be used in: - setProperty - setHeader From bb7c9dc1770190b94d29e6e74fb1876e4a284730 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 19:26:16 +0100 Subject: [PATCH 06/16] refactor: minor --- .../extractors/ApiKeyExpressionExtractor.java | 6 +----- .../balancer/PolyglotSessionIdExtractor.java | 6 +----- .../core/interceptor/flow/ForInterceptor.java | 6 +----- .../core/interceptor/flow/IfInterceptor.java | 6 +----- .../core/interceptor/flow/choice/Case.java | 7 ++----- .../idempotency/IdempotencyInterceptor.java | 15 ++++--------- .../ratelimit/RateLimitInterceptor.java | 6 +----- .../core/lang/ExchangeExpression.java | 6 +----- .../core/lang/TemplateExchangeExpression.java | 6 +----- .../core/openapi/serviceproxy/APIProxy.java | 6 +----- .../lang/AbstractExchangeExpressionTest.java | 21 +++---------------- .../JsonpathExchangeExpressionTest.java | 18 ++++------------ .../xpath/XPathExchangeExpressionTest.java | 4 ++-- 13 files changed, 23 insertions(+), 90 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java index 9434ce704e..1def43cd74 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java @@ -56,11 +56,7 @@ public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XML @Override public void init(Router router) { -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, expression); -======= - exchangeExpression = expression(router, language, expression); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = expression(new InterceptorAdapter(router,namespaces), language, expression); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java index 1e49110213..2b8969341e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java @@ -35,11 +35,7 @@ public class PolyglotSessionIdExtractor extends AbstractXmlElement implements Se public void init(Router router) { if (sessionSource != null && !sessionSource.isEmpty()) { -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router), language, sessionSource); -======= - exchangeExpression = expression(router, language, sessionSource); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = ExchangeExpression.expression(new InterceptorAdapter(router), language, sessionSource); } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java index 47456375cc..cdafbeb686 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/ForInterceptor.java @@ -57,11 +57,7 @@ public class ForInterceptor extends AbstractFlowWithChildrenInterceptor { public void init() { super.init(); try { -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(this, language, in); -======= - exchangeExpression = expression(router, language, in); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = expression(this, language, in); } catch (ConfigurationException ce) { throw new ConfigurationException(ce.getMessage() + """ diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java index 9a8a0077b1..8feb81af59 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java @@ -55,11 +55,7 @@ public IfInterceptor() { @Override public void init() { super.init(); -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(this, language, test); -======= - exchangeExpression = expression(router, language, test); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = expression(this, language, test); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java index 20a0fc2b1b..52a6129df0 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java @@ -15,6 +15,7 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.config.spring.*; import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.Interceptor.Flow; @@ -38,11 +39,7 @@ public class Case extends InterceptorContainer implements XMLNamespaceSupport { private Namespaces namespaces; public void init(Router router) { -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance( new InterceptorAdapter(router,namespaces), language, test); -======= - exchangeExpression = expression(router, language, test); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = ExchangeExpression.expression( new InterceptorAdapter(router,namespaces), language, test); } boolean evaluate(Exchange exc, Flow flow) { diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java index c8e41cdd0a..6c7c380fc9 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/idempotency/IdempotencyInterceptor.java @@ -27,12 +27,8 @@ import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; import static com.predic8.membrane.core.interceptor.Outcome.*; import static com.predic8.membrane.core.interceptor.Outcome.ABORT; -<<<<<<< HEAD -======= -import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; -import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; -import static com.predic8.membrane.core.lang.ExchangeExpression.expression; ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d +import static com.predic8.membrane.core.lang.ExchangeExpression.*; + /** * @description

Prevents duplicate request processing based on a dynamic idempotency key.

@@ -54,11 +50,8 @@ public class IdempotencyInterceptor extends AbstractLanguageInterceptor { @Override public void init() { super.init(); -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(this, language, key); -======= - exchangeExpression = expression(router, language, key); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + + exchangeExpression = expression(this, language, key); processedKeys = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(expiration, TimeUnit.SECONDS) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java index f5ef2e88f8..429dfbc8b0 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java @@ -94,11 +94,7 @@ protected ExchangeExpression getExchangeExpression() { // If there is no expression use the client IP if (expression.isEmpty()) return null; -<<<<<<< HEAD - return ExchangeExpression.newInstance(this, language, expression); -======= - return expression(router, language, expression); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + return expression(this, language, expression); } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java index 5400d854a9..e3721021f1 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java @@ -49,7 +49,6 @@ enum Language {GROOVY, SPEL, XPATH, JSONPATH} */ T evaluate(Exchange exchange, Interceptor.Flow flow, Class type) throws ExchangeExpressionException; -<<<<<<< HEAD /** * Clients of this class should pass an interceptor if possible. Otherwise use the InterceptorAdapter to wrap it. * There is no convenience method on purpose to make the clients pass the interceptor. From the interceptor you can always get the router. @@ -58,10 +57,7 @@ enum Language {GROOVY, SPEL, XPATH, JSONPATH} * @param expression * @return */ - static ExchangeExpression newInstance(Interceptor interceptor, Language language, String expression) { -======= - static ExchangeExpression expression(Router router, Language language, String expression) { ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + static ExchangeExpression expression(Interceptor interceptor, Language language, String expression) { return switch (language) { case GROOVY -> new GroovyExchangeExpression(interceptor, expression); case SPEL -> new SpELExchangeExpression(expression,null); diff --git a/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java index 5ed8bca53e..546b4ac591 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/TemplateExchangeExpression.java @@ -97,11 +97,7 @@ protected static List parseTokens(Interceptor interceptor, Language langu } String expr = m.group(3); if (expr != null) { -<<<<<<< HEAD - tokens.add(new Expression(ExchangeExpression.newInstance(interceptor, language, expr))); -======= - tokens.add(new Expression(expression(router, language, expr))); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + tokens.add(new Expression(expression(interceptor, language, expr))); } } log.debug("Tokens: {}", tokens); diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java index c83c503e70..c8772620e2 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java @@ -84,11 +84,7 @@ public void setSpecs(List specs) { public void init() { super.init(); if (test != null && !test.isEmpty()) { -<<<<<<< HEAD - exchangeExpression = ExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, test); -======= - exchangeExpression = expression(router, language, test); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + exchangeExpression = expression(new InterceptorAdapter(router,namespaces), language, test); } key = new APIProxyKey(key, exchangeExpression, !specs.isEmpty()); initOpenAPI(); diff --git a/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java index e9ac44ec54..9a111388e2 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/AbstractExchangeExpressionTest.java @@ -66,29 +66,14 @@ static void tearDown() { protected abstract Language getLanguage(); protected Object evalObject(String expression) { -<<<<<<< HEAD - return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) - .evaluate(exchange,flow, Object.class); + return expression(new InterceptorAdapter(router), getLanguage(),expression).evaluate(exchange,flow, Object.class); } protected boolean evalBool(String expression) { - return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) - .evaluate(exchange,flow, Boolean.class); + return expression(new InterceptorAdapter(router), getLanguage(),expression).evaluate(exchange,flow, Boolean.class); } protected String evalString(String expression) { - return ExchangeExpression.newInstance(new InterceptorAdapter(router), getLanguage(),expression) - .evaluate(exchange,flow, String.class); -======= - return expression(router, getLanguage(),expression).evaluate(exchange,flow, Object.class); - } - - protected boolean evalBool(String expression) { - return expression(router, getLanguage(),expression).evaluate(exchange,flow, Boolean.class); - } - - protected String evalString(String expression) { - return expression(router, getLanguage(),expression).evaluate(exchange,flow, String.class); ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d + return expression(new InterceptorAdapter(router), getLanguage(),expression).evaluate(exchange,flow, String.class); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java index e9ae8f688a..03e67954bd 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpressionTest.java @@ -117,33 +117,23 @@ void emptyBodyForBoolean() throws URISyntaxException { @Test void wrongContentType() throws URISyntaxException { -<<<<<<< HEAD - assertEquals("", ExchangeExpression.newInstance(new InterceptorAdapter(router), JSONPATH, "$") + assertEquals("", expression(new InterceptorAdapter(router), JSONPATH, "$") .evaluate(Request.post("/foo").contentType(TEXT_XML).buildExchange(), REQUEST, String.class)); } - private static T evaluateWithEmptyBodyFor(Class type) throws URISyntaxException { - return ExchangeExpression.newInstance(new InterceptorAdapter(router), JSONPATH, "$") -======= - assertEquals("", expression(router, JSONPATH, "$") - .evaluate(post("/foo").contentType(TEXT_XML).buildExchange(), REQUEST, String.class)); - } - @Test void array() throws URISyntaxException { - var expr = expression(router, JSONPATH, "$[0]"); + var expr = expression( new InterceptorAdapter(router), JSONPATH, "$[0]"); assertEquals(1, expr.evaluate(post("/foo").json("[1,2,3]").buildExchange(), REQUEST, Integer.class)); } @Test void number() throws URISyntaxException { - var expr = expression(router, JSONPATH, "$"); + var expr = expression(new InterceptorAdapter(router), JSONPATH, "$"); assertEquals(314, expr.evaluate(post("/foo").json("314").buildExchange(), REQUEST, Integer.class)); } private static T evaluateWithEmptyBodyFor(Class type) throws URISyntaxException { - return expression(router, JSONPATH, "$") ->>>>>>> f78bb2c5a6937831602bd693024f03f9f2acad2d - .evaluate(get("/foo").buildExchange(), REQUEST, type); + return expression(new InterceptorAdapter(router), JSONPATH, "$").evaluate(get("/foo").buildExchange(), REQUEST, type); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java index 4cd6ed1732..cd54d324a8 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpressionTest.java @@ -28,7 +28,7 @@ import static com.predic8.membrane.core.http.Request.*; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; -import static com.predic8.membrane.core.lang.ExchangeExpression.newInstance; +import static com.predic8.membrane.core.lang.ExchangeExpression.expression; import static org.junit.jupiter.api.Assertions.*; class XPathExchangeExpressionTest extends AbstractExchangeExpressionTest { @@ -131,7 +131,7 @@ void setup() throws URISyntaxException { @Test void localName() { - assertEquals("Trevor", newInstance( new InterceptorAdapter(router),getLanguage(), + assertEquals("Trevor", expression( new InterceptorAdapter(router),getLanguage(), "//*[local-name()='firstname']") .evaluate(pExc, REQUEST, String.class)); } From a3075f972caee02cf0139fb6138994c4da1293c9 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 19:38:03 +0100 Subject: [PATCH 07/16] refactor: minor --- .../balancer/PolyglotSessionIdExtractor.java | 16 +++++++--------- .../core/interceptor/flow/choice/Case.java | 14 ++++++-------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java index 2b8969341e..f718be179a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/PolyglotSessionIdExtractor.java @@ -13,14 +13,12 @@ limitations under the License. */ package com.predic8.membrane.core.interceptor.balancer; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.annot.MCElement; -import com.predic8.membrane.annot.Required; -import com.predic8.membrane.core.Router; -import com.predic8.membrane.core.config.AbstractXmlElement; -import com.predic8.membrane.core.exchange.Exchange; -import com.predic8.membrane.core.interceptor.Interceptor.Flow; -import com.predic8.membrane.core.interceptor.lang.Polyglot; +import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.*; +import com.predic8.membrane.core.config.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.Interceptor.*; +import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; @@ -35,7 +33,7 @@ public class PolyglotSessionIdExtractor extends AbstractXmlElement implements Se public void init(Router router) { if (sessionSource != null && !sessionSource.isEmpty()) { - exchangeExpression = ExchangeExpression.expression(new InterceptorAdapter(router), language, sessionSource); + exchangeExpression = expression(new InterceptorAdapter(router), language, sessionSource); } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java index 52a6129df0..9447442183 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java @@ -14,18 +14,16 @@ package com.predic8.membrane.core.interceptor.flow.choice; import com.predic8.membrane.annot.*; -import com.predic8.membrane.core.Router; -import com.predic8.membrane.core.config.spring.*; -import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.Interceptor.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.Interceptor.Flow; import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.slf4j.*; -import static com.predic8.membrane.core.lang.ExchangeExpression.Language.SPEL; +import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; import static com.predic8.membrane.core.lang.ExchangeExpression.expression; @MCElement(name = "case", topLevel = false) @@ -39,7 +37,7 @@ public class Case extends InterceptorContainer implements XMLNamespaceSupport { private Namespaces namespaces; public void init(Router router) { - exchangeExpression = ExchangeExpression.expression( new InterceptorAdapter(router,namespaces), language, test); + exchangeExpression = expression( new InterceptorAdapter(router,namespaces), language, test); } boolean evaluate(Exchange exc, Flow flow) { From b80838264d2853ffb27f1ba1909578436975a22d Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 20:02:41 +0100 Subject: [PATCH 08/16] refactor: minor --- .../com/predic8/membrane/core/lang/ExchangeExpression.java | 1 + .../membrane/core/util/xml/parser/HardenedXmlParser.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java index e3721021f1..d6f795d07a 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java @@ -65,6 +65,7 @@ static ExchangeExpression expression(Interceptor interceptor, Language language, case JSONPATH -> new JsonpathExchangeExpression(expression); }; } + /** * Allows to pass an Interceptor as an argument where there is no interceptor e.g. Target */ diff --git a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java index ca27f80be4..51313588ff 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -70,6 +70,10 @@ private static DocumentBuilderFactory createFactory() { return f; } + /** + * Creates a new DocumentBuilder for XML parsing. Access is not thread-safe! + * @return + */ private DocumentBuilder newBuilder() { try { DocumentBuilder builder = factory.newDocumentBuilder(); From 7a65c481a6256b72e52f90312fd322a9c5dfbc4b Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 2 Nov 2025 20:15:34 +0100 Subject: [PATCH 09/16] refactor: minor --- .../SetPropertyInterceptorXPathTest.java | 127 +++++ distribution/conf/apis.yaml | 509 ------------------ .../conf/convert-request-to-new-version.xslt | 25 - .../conf/convert-response-to-old-version.xslt | 20 - distribution/conf/k8s-proxies.xml | 0 distribution/conf/security-api-v1.yml | 23 - distribution/conf/simple.yaml | 27 - 7 files changed, 127 insertions(+), 604 deletions(-) create mode 100644 core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java delete mode 100644 distribution/conf/apis.yaml delete mode 100644 distribution/conf/convert-request-to-new-version.xslt delete mode 100644 distribution/conf/convert-response-to-old-version.xslt delete mode 100644 distribution/conf/k8s-proxies.xml delete mode 100644 distribution/conf/security-api-v1.yml delete mode 100644 distribution/conf/simple.yaml diff --git a/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java new file mode 100644 index 0000000000..7473c41b3d --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java @@ -0,0 +1,127 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.lang.xpath; + +import com.fasterxml.jackson.databind.*; +import com.predic8.membrane.core.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.lang.*; +import org.jetbrains.annotations.*; +import org.junit.jupiter.api.*; + +import java.util.*; + +import static com.predic8.membrane.core.http.Request.*; +import static com.predic8.membrane.core.interceptor.Outcome.*; +import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SetPropertyInterceptorXPathTest { + + private ObjectMapper om = new ObjectMapper(); + private Exchange exc; + + @Nested + class WithoutNamespaces { + + @BeforeEach + void setup() throws Exception { + exc = post("/person").xml(""" + + Trevor + + """).buildExchange(); + } + + @Test + void normal() { + var interceptor = getInterceptor(null,"${//firstname}"); + assertEquals(CONTINUE, interceptor.handleRequest(exc)); + assertEquals("Trevor", exc.getProperty("firstname").toString()); + } + } + + @Nested + class WithNamespaces { + + @BeforeEach + void setup() throws Exception { + exc = post("/person").xml(""" + + Trevor + baz + + """).buildExchange(); + } + + @Test + void noNamespacesDeclaredQueryWithPrefix() { + var interceptor = getInterceptor(null,"${//p8:firstname}"); + assertEquals(CONTINUE, interceptor.handleRequest(exc)); + assertEquals("", exc.getProperty("firstname").toString()); + } + + @Test + void noNamespacesDeclaredQueryWithLocalName() { + var interceptor = getInterceptor(null,"${//*[local-name() = 'firstname']}"); + assertEquals(CONTINUE, interceptor.handleRequest(exc)); + assertEquals("Trevor", exc.getProperty("firstname").toString()); + } + + @Test + void nsUri() { + var interceptor = getInterceptor(null,"${//*[namespace-uri() = 'https://predic8.de/other']}"); + assertEquals(CONTINUE, interceptor.handleRequest(exc)); + assertEquals("baz", exc.getProperty("firstname").toString()); + } + + @Test + void nsPrefixed() { + var interceptor = getInterceptor(getNamespaces(),"${//p8:firstname}"); + assertEquals(CONTINUE, interceptor.handleRequest(exc)); + assertEquals("Trevor", exc.getProperty("firstname").toString()); + } + + @Test + void unknownPrefix() throws Exception { + var interceptor = getInterceptor(getNamespaces(),"${//unknown:firstname}"); + assertEquals(ABORT, interceptor.handleRequest(exc)); + assertEquals(500, exc.getResponse().getStatusCode()); + String body = exc.getResponse().getBodyAsStringDecoded(); + assertTrue(body.contains("${//unknown:firstname}")); + assertTrue(body.contains("unknown")); + } + } + + private static @NotNull SetPropertyInterceptor getInterceptor(Namespaces namespaces, String value) { + var i = new SetPropertyInterceptor(); + i.setNamespaces(namespaces); + i.setLanguage(XPATH); + i.setFieldName("firstname"); + i.setValue(value); + i.init(new Router()); + return i; + } + + private static @NotNull Namespaces getNamespaces() { + var p8 = new Namespaces.Namespace(); + p8.prefix = "p8"; + p8.uri = "https://predic8.de"; + var ns = new Namespaces(); + ns.setNamespace(List.of(p8)); + return ns; + } +} diff --git a/distribution/conf/apis.yaml b/distribution/conf/apis.yaml deleted file mode 100644 index 7113e23a84..0000000000 --- a/distribution/conf/apis.yaml +++ /dev/null @@ -1,509 +0,0 @@ -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -kind: api -spec: - port: 2000 - target: - url: https://api.predic8.de - ---- - -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -kind: api -spec: - port: 2001 - path: - uri: /fruit/{id} - target: - url: https://api.predic8.de/shop/v2/products/${pathParam.id} - ---- - -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2002 - specs: - - openapi: - location: "fruitshop-api.yml" - validateRequests: yes - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2003 - flow: - - log: - message: "Headers: ${header}" - target: - url: https://api.predic8.de - ---- - -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2004 - path: - uri: /shop - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2005 - specs: - - openapi: - location: "fruitshop-api.yml" - validateRequests: yes - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2007 - method: POST - flow: - - response: - - static: - src: | - POST is blocked! - - return: - statusCode: 405 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2007 - path: - uri: /shop/v2/products/.* - isRegExp: true - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2007 - path: - uri: /shop - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2007 - host: www.predic8.de - flow: - - response: - - static: - src: Homepage - - return: - statusCode: 200 - path: - uri: /shop - target: - url: https://api.predic8.de - - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2008 - test: params.city == 'Paris' - flow: - - response: - - static: - src: Oui! - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2009 - path: - uri: /health - flow: - - response: - - static: - src: I'm good. - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2010 - path: - uri: /nothing - flow: - - response: - - static: - src: "Nothing to see here." - - return: - statusCode: 404 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2011 - flow: - - rewriter: - - map: - from: ^/fruitshop/(.*) - to: /shop/v2/$1 - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2012 - flow: - - groovy: - src: | - println "I'm executed in the ${flow} flow" - println "HTTP Headers:\n${header}" - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2013 - flow: - - groovy: - src: | - sites = ["https://api.predic8.de","https://membrane-api.io","https://predic8.de"] - Collections.shuffle sites - exchange.destinations = sites - target: {} - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2014 - flow: - - groovy: - src: | - Response.ok() - .contentType("application/json") - .header("X-Foo", "bar") - .body(""" - { - "success": true - }""") - .build() - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2015 - flow: - - javascript: - src: | - console.log("------------ Headers: -------------"); - var fields = header.getAllHeaderFields(); - for (var i = 0; i < fields.length; i++) { - console.log(fields[i]); - } - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2016 - flow: - - javascript: - src: | - var body = JSON.stringify({ - foo: 7, - bar: 42 - }); - Response.ok(body).contentType("application/json").build(); - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2017 - flow: - - response: - - setHeader: - name: Access-Control-Allow-Origin - value: "*" - - setHeader: - name: Access-Control-Allow-Methods - value: GET - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2018 - flow: - - response: - - setHeader: - name: X-Product-Id - value: ${jsonPath('$.id')} - - setHeader: - name: X-Product-Name - value: ${$.name} - language: jsonpath - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2019 - flow: - - response: - - headerFilter: - rules: - - include: - pattern: "X-XSS-Protection" - - exclude: - pattern: "X-.*" - target: - url: https://www.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2020 - flow: - - request: - - template: - contentType: application/json - pretty: true - src: | - { "answer": ${params.answer} } - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2021 - flow: - - request: - - template: - contentType: text/plain - src: | - City: ${json.city} - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2022 - flow: - - request: - - template: - contentType: application/json - src: | - { - "destination": "${json.city}" - } - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2023 - flow: - - request: - - template: - contentType: application/xml - src: | - - ${json.city} - - - return: - statusCode: 200 - ---- -spec: - port: 2024 - flow: - - request: - - javascript: - src: | - function convertDate(d) { - return d.getFullYear() + "-" + ("0"+(d.getMonth()+1)).slice(-2) + "-" + ("0"+d.getDate()).slice(-2); - } - - ({ - id: json.id, - date: convertDate(new Date(json.date)), - client: json.customer, - total: json.items.map(i => i.quantity * i.price).reduce((a,b) => a+b), - positions: json.items.map(i => ({ - pieces: i.quantity, - price: i.price, - article: i.description - })) - }) - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2025 - flow: - - response: - - beautifier: {} # Response flow is reversed - - template: - contentType: application/xml - src: | - Baz - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2026 - flow: - - response: - - if: - test: statusCode matches '4\d\d' - flow: - - static: - src: "Stupid User!" - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2027 - flow: - - apiKey: - stores: - - keys: - - secret: - value: abc123 - - secret: - value: secret123 - - return: - statusCode: 200 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2028 - flow: - - jwtAuth: - expectedAud: "api://2axxxx16-xxxx-xxxx-xxxx-faxxxxxxxxf0" - jwks: - jwksUris: https://login.microsoftonline.com/common/discovery/keys - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2029 - flow: - - oauth2Resource2: - membrane: - src: https://accounts.google.com - clientId: INSERT_CLIENT_ID - clientSecret: INSERT_CLIENT_SECRET - scope: email profile - subject: sub - - groovy: - src: | - // Get email from OAuth2 and forward it to the backend - def oauth2 = exc.properties.'membrane.oauth2' - header.setValue('X-EMAIL',oauth2.userinfo.email) - target: - url: https://backend - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -#spec: -# port: 2030 -# flow: -# - oauth2authserver: -# location: logindialog -# issuer: http://localhost:2000 -# consentFile: consent.json -# staticUserDataProvider: -# users: -# - user: -# username: john -# password: secret -# email: john@predic8.de -# bearerJwtToken: {} -# claims: -# scopes: -# - scope: -# id: username -# target: -# url: http://backend - - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2031 - flow: - - basicAuthentication: - users: - - user: - username: bob - password: secret - - user: - username: alice - password: secret - target: - host: localhost - port: 8080 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2032 - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 443 - ssl: - keystore: - location: membrane.p12 - password: secret - keyPassword: secret - target: - host: localhost - port: 8080 - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2033 - flow: - - xmlProtection: - maxElementNameLength: 100 - target: - url: https://api.predic8.de - ---- -# yaml-language-server: $schema=../../core/target/classes/com/predic8/membrane/core/config/json/membrane.schema.json -spec: - port: 2034 - flow: - - jsonProtection: - maxDepth: 5 - maxKeyLength: 30 - maxStringLength: 100000 - target: - url: https://api.predic8.de \ No newline at end of file diff --git a/distribution/conf/convert-request-to-new-version.xslt b/distribution/conf/convert-request-to-new-version.xslt deleted file mode 100644 index 3123616690..0000000000 --- a/distribution/conf/convert-request-to-new-version.xslt +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/distribution/conf/convert-response-to-old-version.xslt b/distribution/conf/convert-response-to-old-version.xslt deleted file mode 100644 index f7ae624409..0000000000 --- a/distribution/conf/convert-response-to-old-version.xslt +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/distribution/conf/k8s-proxies.xml b/distribution/conf/k8s-proxies.xml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/distribution/conf/security-api-v1.yml b/distribution/conf/security-api-v1.yml deleted file mode 100644 index 9f2bcae245..0000000000 --- a/distribution/conf/security-api-v1.yml +++ /dev/null @@ -1,23 +0,0 @@ -openapi: '3.0.2' -info: - title: Security Demo API - version: '1.0' - -servers: - - url: http://localhost:2000/security-api/v1/ - -paths: - /: - get: - security: - - apikey: [] - responses: - 200: - description: OK - -components: - securitySchemes: - apikey: - type: apiKey - name: X-Api-Keyz - in: query \ No newline at end of file diff --git a/distribution/conf/simple.yaml b/distribution/conf/simple.yaml deleted file mode 100644 index fbf4b5c7aa..0000000000 --- a/distribution/conf/simple.yaml +++ /dev/null @@ -1,27 +0,0 @@ -openapi: 3.1.0 -info: - title: Simple Parameters API - version: "1.0.0" -servers: - - url: http://localhost - -paths: - /foo: - get: - parameters: - - name: bar - in: query - explode: true - schema: - type: array - items: - type: string - - name: baz - in: query - explode: true - schema: - type: integer - responses: - "200": - description: OK - From c9e9734dcab5155c6eae5fb0b015c02791b9c082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Mon, 3 Nov 2025 15:27:50 +0100 Subject: [PATCH 10/16] add test cases --- .../xml/namespaces/NamespacesExampleTest.java | 117 +++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index 9ce7c07f7b..4ad0a5c519 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -28,8 +28,6 @@ protected String getExampleDirName() { return "xml/namespaces"; } - // TODO implement other cases as well - @Test void namespaceAwareXPathExtraction() { String xmlBody = """ @@ -76,4 +74,119 @@ void differentCity() { // @formatter:on } + @Test + void differentPrefixesStillMatch() { + String xmlBody = """ + + Kim + + Bonn + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("Kim from Bonn")); + // @formatter:on + } + + @Test + void defaultNamespaceOnPerson() { + String xmlBody = """ + + Udo + + Hamburg + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("Udo from Hamburg")); + // @formatter:on + } + + @Test + void noNamespacesShouldNotMatch() { + String xmlBody = """ + + Max +
+ Cologne +
+
+ """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("from")); + // @formatter:on + } + + @Test + void wrongNamespaceOnPerson() { + String xmlBody = """ + + Hans + + Cologne + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("from Cologne")); + // @formatter:on + } + + @Test + void wrongNamespaceOnAddress() { + String xmlBody = """ + + Hans + + Cologne + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .body(containsString("Hans from")); + // @formatter:on + } + } From 4aebbb07550a7e69abaeb71f4b06c5c91d97681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Mon, 3 Nov 2025 16:09:17 +0100 Subject: [PATCH 11/16] add test case for header (fails) --- .../xml/namespaces/NamespacesExampleTest.java | 50 ++++++------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index 4ad0a5c519..375afdf5b0 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -19,6 +19,7 @@ import static com.predic8.membrane.core.http.MimeType.TEXT_XML; import static io.restassured.RestAssured.given; import static io.restassured.filter.log.LogDetail.ALL; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsString; public class NamespacesExampleTest extends AbstractSampleMembraneStartStopTestcase { @@ -120,35 +121,13 @@ void defaultNamespaceOnPerson() { // @formatter:on } - @Test - void noNamespacesShouldNotMatch() { - String xmlBody = """ - - Max -
- Cologne -
-
- """; - - // @formatter:off - given() - .contentType(TEXT_XML) - .body(xmlBody) - .post("http://localhost:2000") - .then() - .log().ifValidationFails(ALL) - .statusCode(200) - .body(containsString("from")); - // @formatter:on - } @Test - void wrongNamespaceOnPerson() { + void wrongNamespaceOnAddress() { String xmlBody = """ - + Hans - + Cologne @@ -162,20 +141,20 @@ void wrongNamespaceOnPerson() { .then() .log().ifValidationFails(ALL) .statusCode(200) - .body(containsString("from Cologne")); + .body(containsString("Hans from")); // @formatter:on } @Test - void wrongNamespaceOnAddress() { + void headerIdIsSet() { String xmlBody = """ - - Hans - - Cologne - - - """; + + Hans + + Cologne + + + """; // @formatter:off given() @@ -185,7 +164,8 @@ void wrongNamespaceOnAddress() { .then() .log().ifValidationFails(ALL) .statusCode(200) - .body(containsString("Hans from")); + .header("id", equalTo("77")) + .body(containsString("Hans from Cologne")); // @formatter:on } From aa3ad0d8803a0f0bcee60d0b3828cba578cb5d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Mon, 3 Nov 2025 16:41:35 +0100 Subject: [PATCH 12/16] add test case for header --- distribution/examples/xml/namespaces/proxies.xml | 2 ++ .../test/xml/namespaces/NamespacesExampleTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/distribution/examples/xml/namespaces/proxies.xml b/distribution/examples/xml/namespaces/proxies.xml index 059d75cb83..836e0ce1ce 100644 --- a/distribution/examples/xml/namespaces/proxies.xml +++ b/distribution/examples/xml/namespaces/proxies.xml @@ -35,12 +35,14 @@ + + diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java index 375afdf5b0..8f6f5bb6a1 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -164,7 +164,7 @@ void headerIdIsSet() { .then() .log().ifValidationFails(ALL) .statusCode(200) - .header("id", equalTo("77")) + .header("x-seen-request-id", equalTo("77")) .body(containsString("Hans from Cologne")); // @formatter:on } From 429e21adaee5dfea171919efe6695013d6116399 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Mon, 3 Nov 2025 20:35:06 +0100 Subject: [PATCH 13/16] refactor: minor --- .../core/lang/xpath/SetPropertyInterceptorXPathTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java index 7473c41b3d..265d01023b 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java @@ -31,7 +31,6 @@ class SetPropertyInterceptorXPathTest { - private ObjectMapper om = new ObjectMapper(); private Exchange exc; @Nested @@ -96,7 +95,7 @@ void nsPrefixed() { } @Test - void unknownPrefix() throws Exception { + void unknownPrefix() { var interceptor = getInterceptor(getNamespaces(),"${//unknown:firstname}"); assertEquals(ABORT, interceptor.handleRequest(exc)); assertEquals(500, exc.getResponse().getStatusCode()); From 8e27a36144e6af4268c8cf14c6e0284f0168941c Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Wed, 5 Nov 2025 12:25:50 +0100 Subject: [PATCH 14/16] refactor: xmlConfig instead of just namespaces --- .../membrane/core/config/XMLElement.java | 4 +-- .../lang => config/xml}/Namespaces.java | 4 +-- .../membrane/core/config/xml/XmlConfig.java | 18 +++++++++++++ ...LNamespaceSupport.java => XMLSupport.java} | 8 +++--- .../extractors/ApiKeyExpressionExtractor.java | 21 +++++++++------- .../core/interceptor/flow/IfInterceptor.java | 21 ++++++++-------- .../core/interceptor/flow/choice/Case.java | 22 ++++++++-------- .../lang/AbstractLanguageInterceptor.java | 21 +++++++++------- .../core/lang/ExchangeExpression.java | 24 +++++++++++------- .../lang/xpath/XPathExchangeExpression.java | 16 ++++++------ .../core/openapi/serviceproxy/APIProxy.java | 24 ++++++++++-------- .../core/proxies/AbstractServiceProxy.java | 25 +++++++++++-------- .../core/interceptor/lang/NamespacesTest.java | 1 + .../SetPropertyInterceptorXPathTest.java | 7 ++++-- 14 files changed, 131 insertions(+), 85 deletions(-) rename core/src/main/java/com/predic8/membrane/core/{interceptor/lang => config/xml}/Namespaces.java (96%) create mode 100644 core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java rename core/src/main/java/com/predic8/membrane/core/interceptor/{XMLNamespaceSupport.java => XMLSupport.java} (79%) diff --git a/core/src/main/java/com/predic8/membrane/core/config/XMLElement.java b/core/src/main/java/com/predic8/membrane/core/config/XMLElement.java index 2e91bb7f46..49ba71b217 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/XMLElement.java +++ b/core/src/main/java/com/predic8/membrane/core/config/XMLElement.java @@ -20,8 +20,8 @@ public interface XMLElement { - public abstract XMLElement parse(XMLStreamReader token) throws Exception; + XMLElement parse(XMLStreamReader token) throws Exception; - public abstract void write(XMLStreamWriter out) throws XMLStreamException; + void write(XMLStreamWriter out) throws XMLStreamException; } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java similarity index 96% rename from core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java rename to core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java index a077c53022..8f230f2996 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/Namespaces.java +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package com.predic8.membrane.core.interceptor.lang; +package com.predic8.membrane.core.config.xml; import com.predic8.membrane.annot.*; @@ -21,7 +21,7 @@ import static javax.xml.XMLConstants.NULL_NS_URI; -@MCElement(name="namespaces", topLevel = true) +@MCElement(name="namespaces", topLevel = false) public class Namespaces { private List namespaces; diff --git a/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java b/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java new file mode 100644 index 0000000000..7561059b5d --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/XmlConfig.java @@ -0,0 +1,18 @@ +package com.predic8.membrane.core.config.xml; + +import com.predic8.membrane.annot.*; + +@MCElement(name="xmlConfig",topLevel = true) +public class XmlConfig { + + private Namespaces namespaces; + + @MCChildElement + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + + public Namespaces getNamespaces() { + return namespaces; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLSupport.java similarity index 79% rename from core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java rename to core/src/main/java/com/predic8/membrane/core/interceptor/XMLSupport.java index b0910b164f..58acf075ff 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/XMLNamespaceSupport.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLSupport.java @@ -14,11 +14,11 @@ package com.predic8.membrane.core.interceptor; -import com.predic8.membrane.core.interceptor.lang.*; +import com.predic8.membrane.core.config.xml.*; -public interface XMLNamespaceSupport { +public interface XMLSupport { - void setNamespaces(Namespaces namespaces); + void setXmlConfig(XmlConfig xmlConfig); - Namespaces getNamespaces(); + XmlConfig getXmlConfig(); } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java index 1def43cd74..4a1ae88d9e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyExpressionExtractor.java @@ -15,6 +15,7 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.lang.*; @@ -47,16 +48,16 @@ * @topic 3. Security and Validation */ @MCElement(name="expressionExtractor", topLevel = false) -public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XMLNamespaceSupport { +public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XMLSupport { private String expression = ""; private Language language = SPEL; private ExchangeExpression exchangeExpression; - private Namespaces namespaces; + private XmlConfig xmlConfig; @Override public void init(Router router) { - exchangeExpression = expression(new InterceptorAdapter(router,namespaces), language, expression); + exchangeExpression = expression(new InterceptorAdapter(router, xmlConfig), language, expression); } @Override @@ -98,15 +99,17 @@ public void setExpression(String expression) { } /** - * Declaration of XML namespaces for XPath expressions. - * @param namespaces + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig */ + @Override @MCChildElement(allowForeign = true) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java index 8feb81af59..f00c84e7f6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/IfInterceptor.java @@ -15,12 +15,11 @@ package com.predic8.membrane.core.interceptor.flow; import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; -import com.predic8.membrane.core.lang.xpath.*; import org.slf4j.*; import static com.predic8.membrane.core.exceptions.ProblemDetails.*; @@ -40,13 +39,14 @@ * @topic 1. Proxies and Flow */ @MCElement(name = "if") -public class IfInterceptor extends AbstractFlowWithChildrenInterceptor implements XMLNamespaceSupport { +public class IfInterceptor extends AbstractFlowWithChildrenInterceptor implements XMLSupport { private static final Logger log = LoggerFactory.getLogger(IfInterceptor.class); private String test; private Language language = SPEL; private ExchangeExpression exchangeExpression; + private XmlConfig xmlConfig; public IfInterceptor() { name = "if"; @@ -135,16 +135,17 @@ public String getShortDescription() { } /** - * XML namespaces to be used in expressions. + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig */ - protected Namespaces namespaces; - + @Override @MCChildElement(allowForeign = true,order = 10) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java index 9447442183..58cd7dc1d3 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/flow/choice/Case.java @@ -15,10 +15,10 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.Interceptor.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; import org.slf4j.*; @@ -27,17 +27,17 @@ import static com.predic8.membrane.core.lang.ExchangeExpression.expression; @MCElement(name = "case", topLevel = false) -public class Case extends InterceptorContainer implements XMLNamespaceSupport { +public class Case extends InterceptorContainer implements XMLSupport { private static final Logger log = LoggerFactory.getLogger(Case.class); private String test; private Language language = SPEL; private ExchangeExpression exchangeExpression; - private Namespaces namespaces; + private XmlConfig xmlConfig; public void init(Router router) { - exchangeExpression = expression( new InterceptorAdapter(router,namespaces), language, test); + exchangeExpression = expression( new InterceptorAdapter(router,xmlConfig), language, test); } boolean evaluate(Exchange exc, Flow flow) { @@ -79,15 +79,17 @@ public void setTest(String test) { } /** - * Declaration of XML namespaces for XPath expressions. - * @param namespaces + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig */ + @Override @MCChildElement(allowForeign = true,order = 10) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java index 50516a2dd5..6ec9d75bcf 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/lang/AbstractLanguageInterceptor.java @@ -15,18 +15,19 @@ package com.predic8.membrane.core.interceptor.lang; import com.predic8.membrane.annot.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; -public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLNamespaceSupport { +public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLSupport { /** * SpEL is default */ protected Language language = SPEL; - protected Namespaces namespaces; + protected XmlConfig xmlConfig; public Language getLanguage() { return language; @@ -43,15 +44,17 @@ public void setLanguage(Language language) { } /** - * Declaration of XML namespaces for XPath expressions. - * @param namespaces + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig */ - @MCChildElement(allowForeign = true) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + @Override + @MCChildElement(allowForeign = true,order = 10) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java index d6f795d07a..b96947bc32 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/ExchangeExpression.java @@ -14,10 +14,11 @@ package com.predic8.membrane.core.lang; +import com.predic8.membrane.annot.*; import com.predic8.membrane.core.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.groovy.*; import com.predic8.membrane.core.lang.jsonpath.*; import com.predic8.membrane.core.lang.spel.*; @@ -69,27 +70,32 @@ static ExchangeExpression expression(Interceptor interceptor, Language language, /** * Allows to pass an Interceptor as an argument where there is no interceptor e.g. Target */ - class InterceptorAdapter extends AbstractInterceptor implements XMLNamespaceSupport{ + class InterceptorAdapter extends AbstractInterceptor implements XMLSupport { - private Namespaces namespaces; + private XmlConfig xmlConfig; public InterceptorAdapter(Router router) { this.router = router; } - public InterceptorAdapter(Router router, Namespaces namespaces) { + public InterceptorAdapter(Router router, XmlConfig xmlConfig) { this.router = router; - this.namespaces = namespaces; + this.xmlConfig = xmlConfig; } + /** + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig + */ @Override - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + @MCChildElement(allowForeign = true,order = 10) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } @Override - public Namespaces getNamespaces() { - return namespaces; + public XmlConfig getXmlConfig() { + return xmlConfig; } } } diff --git a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java index 139ced5d93..0dc662d887 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java @@ -14,10 +14,10 @@ package com.predic8.membrane.core.lang.xpath; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.util.xml.*; import com.predic8.membrane.core.util.xml.parser.*; @@ -35,7 +35,7 @@ public class XPathExchangeExpression extends AbstractExchangeExpression { private static final XmlParser parser = HardenedXmlParser.getInstance(); - private Namespaces namespaces; + private XmlConfig xmlConfig; // Let all expressions share the same XPathFactory. private static final XPathFactory factory = XPathFactory.newInstance(); @@ -43,8 +43,8 @@ public class XPathExchangeExpression extends AbstractExchangeExpression { public XPathExchangeExpression(Interceptor interceptor, String xpath) { super(xpath); - if (interceptor instanceof XMLNamespaceSupport xns) { - namespaces = xns.getNamespaces(); + if (interceptor instanceof XMLSupport xns) { + xmlConfig = xns.getXmlConfig(); } } @@ -86,14 +86,14 @@ private Object evalutateAndCast(Message msg, QName xmlType) throws XPathExpressi // XPath is not thread safe! Therefore, every time the factory is called! XPath xPath = factory.newXPath(); - if (namespaces != null) { - xPath.setNamespaceContext(namespaces.getNamespaceContext()); + if (xmlConfig != null) { + xPath.setNamespaceContext(xmlConfig.getNamespaces().getNamespaceContext()); } return xPath.evaluate(expression, parser.parse(XMLUtil.getInputSource(msg)), xmlType); } - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } } diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java index c8772620e2..022e8e1aa4 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/serviceproxy/APIProxy.java @@ -20,6 +20,8 @@ import com.predic8.membrane.annot.MCChildElement; import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.annot.MCTextContent; +import com.predic8.membrane.core.config.xml.*; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression.*; @@ -27,6 +29,7 @@ import com.predic8.membrane.core.proxies.ServiceProxy; import com.predic8.membrane.core.util.ConfigurationException; import com.predic8.membrane.core.util.URIFactory; +import com.predic8.membrane.core.util.xml.*; import io.swagger.v3.oas.models.servers.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +45,7 @@ * @topic 1. Proxies and Flow */ @MCElement(name = "api") -public class APIProxy extends ServiceProxy implements Polyglot { +public class APIProxy extends ServiceProxy implements Polyglot, XMLSupport { private static final Logger log = LoggerFactory.getLogger(APIProxy.class.getName()); @@ -58,7 +61,7 @@ public class APIProxy extends ServiceProxy implements Polyglot { private String test; private String id; private ApiDescription description; - private Namespaces namespaces; + private XmlConfig xmlConfig; protected Map apiRecords = new LinkedHashMap<>(); @@ -84,7 +87,7 @@ public void setSpecs(List specs) { public void init() { super.init(); if (test != null && !test.isEmpty()) { - exchangeExpression = expression(new InterceptorAdapter(router,namespaces), language, test); + exchangeExpression = expression(new InterceptorAdapter(router, xmlConfig), language, test); } key = new APIProxyKey(key, exchangeExpression, !specs.isEmpty()); initOpenAPI(); @@ -239,15 +242,16 @@ public String getContent() { } /** - * Declaration of XML namespaces for XPath expressions. - * @param namespaces + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig */ - @MCChildElement(allowForeign = true, order = 10) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + @MCChildElement(allowForeign = true,order = 10) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + public XmlConfig getXmlConfig() { + return xmlConfig; } + } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java index b26eedcce8..8af45f2e19 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractServiceProxy.java @@ -18,13 +18,12 @@ import com.predic8.membrane.core.Router; import com.predic8.membrane.core.config.*; import com.predic8.membrane.core.config.security.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.*; -import com.predic8.membrane.core.interceptor.lang.*; import com.predic8.membrane.core.lang.ExchangeExpression; import com.predic8.membrane.core.lang.ExchangeExpression.*; import com.predic8.membrane.core.lang.TemplateExchangeExpression; -import com.predic8.membrane.core.lang.xpath.*; import com.predic8.membrane.core.transport.ssl.*; import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; @@ -99,7 +98,7 @@ public void setPath(Path path) { *

*/ @MCElement(name = "target", topLevel = false) - public static class Target implements XMLNamespaceSupport { + public static class Target implements XMLSupport { private String host; private int port = -1; private String method; @@ -110,11 +109,11 @@ public static class Target implements XMLNamespaceSupport { private SSLParser sslParser; - protected Namespaces namespaces; + protected XmlConfig xmlConfig; public void init(Router router) { if (url != null) { - exchangeExpression = TemplateExchangeExpression.newInstance(new InterceptorAdapter(router,namespaces), language, url); + exchangeExpression = TemplateExchangeExpression.newInstance(new InterceptorAdapter(router,xmlConfig), language, url); } } @@ -237,13 +236,19 @@ public void setLanguage(ExchangeExpression.Language language) { this.language = language; } - @MCChildElement(allowForeign = true, order = 100) - public void setNamespaces(Namespaces namespaces) { - this.namespaces = namespaces; + /** + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig + */ + @Override + @MCChildElement(allowForeign = true,order = 10) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; } - public Namespaces getNamespaces() { - return namespaces; + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; } } diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java index 88d6c78d41..232e27576a 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java @@ -14,6 +14,7 @@ package com.predic8.membrane.core.interceptor.lang; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.util.*; import org.junit.jupiter.api.*; diff --git a/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java index 265d01023b..915149fb6e 100644 --- a/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java @@ -14,9 +14,10 @@ package com.predic8.membrane.core.lang.xpath; -import com.fasterxml.jackson.databind.*; import com.predic8.membrane.core.*; +import com.predic8.membrane.core.config.xml.*; import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.lang.*; import org.jetbrains.annotations.*; import org.junit.jupiter.api.*; @@ -107,7 +108,9 @@ void unknownPrefix() { private static @NotNull SetPropertyInterceptor getInterceptor(Namespaces namespaces, String value) { var i = new SetPropertyInterceptor(); - i.setNamespaces(namespaces); + XmlConfig xc = new XmlConfig(); + xc.setNamespaces(namespaces); + i.setXmlConfig(xc); i.setLanguage(XPATH); i.setFieldName("firstname"); i.setValue(value); From d05683eb884757c04ae302bd426d8a0204fb0e3c Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Wed, 5 Nov 2025 13:23:13 +0100 Subject: [PATCH 15/16] refactor: minor --- .../java/com/predic8/membrane/core/config/xml/Namespaces.java | 2 +- .../membrane/core/lang/xpath/XPathExchangeExpression.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java index 8f230f2996..75bf5ddc77 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java @@ -32,7 +32,7 @@ public NamespaceContext getNamespaceContext() { } /** - * @description Defines a regex and a replacement for the rewriting of the URI. + * @description Defines XML namespace mappings (prefix to URI) for use in XPath expressions. */ @MCChildElement(allowForeign = false) public void setNamespace(List namespace) { diff --git a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java index 0dc662d887..ee19ebeb8e 100644 --- a/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java +++ b/core/src/main/java/com/predic8/membrane/core/lang/xpath/XPathExchangeExpression.java @@ -86,7 +86,7 @@ private Object evalutateAndCast(Message msg, QName xmlType) throws XPathExpressi // XPath is not thread safe! Therefore, every time the factory is called! XPath xPath = factory.newXPath(); - if (xmlConfig != null) { + if (xmlConfig != null && xmlConfig.getNamespaces() != null) { xPath.setNamespaceContext(xmlConfig.getNamespaces().getNamespaceContext()); } From 04a6490b2b425660b28103e84d7351cc3306b789 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Wed, 5 Nov 2025 13:47:23 +0100 Subject: [PATCH 16/16] refactor: minor --- .../examples/xml/namespaces/proxies.xml | 24 ++++++++++++------- .../examples/xml/namespaces/requests.http | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/distribution/examples/xml/namespaces/proxies.xml b/distribution/examples/xml/namespaces/proxies.xml index 836e0ce1ce..519c3ece52 100644 --- a/distribution/examples/xml/namespaces/proxies.xml +++ b/distribution/examples/xml/namespaces/proxies.xml @@ -4,11 +4,13 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd"> - - - - - + + + + + + + @@ -19,12 +21,18 @@ - - + + + + + + + + - + diff --git a/distribution/examples/xml/namespaces/requests.http b/distribution/examples/xml/namespaces/requests.http index e270a2349f..e9e79af326 100644 --- a/distribution/examples/xml/namespaces/requests.http +++ b/distribution/examples/xml/namespaces/requests.http @@ -2,7 +2,7 @@ POST http://localhost:2000 Content-Type: text/xml - Hans + Herbert Cologne