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/config/xml/Namespaces.java b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java new file mode 100644 index 0000000000..75bf5ddc77 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/config/xml/Namespaces.java @@ -0,0 +1,102 @@ +/* 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.config.xml; + +import com.predic8.membrane.annot.*; + +import javax.xml.namespace.*; +import java.util.*; + +import static javax.xml.XMLConstants.NULL_NS_URI; + +@MCElement(name="namespaces", topLevel = false) +public class Namespaces { + + private List namespaces; + private final NamespaceContextImpl nsContext = new NamespaceContextImpl(); + + public NamespaceContext getNamespaceContext() { + return nsContext; + } + + /** + * @description Defines XML namespace mappings (prefix to URI) for use in XPath expressions. + */ + @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/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/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/XMLSupport.java b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLSupport.java new file mode 100644 index 0000000000..58acf075ff --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/XMLSupport.java @@ -0,0 +1,24 @@ +/* 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.config.xml.*; + +public interface XMLSupport { + + void setXmlConfig(XmlConfig xmlConfig); + + 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 7c1e28953f..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 @@ -13,13 +13,14 @@ 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.config.xml.*; 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; @@ -47,15 +48,16 @@ * @topic 3. Security and Validation */ @MCElement(name="expressionExtractor", topLevel = false) -public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot { +public class ApiKeyExpressionExtractor implements ApiKeyExtractor, Polyglot, XMLSupport { private String expression = ""; private Language language = SPEL; private ExchangeExpression exchangeExpression; + private XmlConfig xmlConfig; @Override public void init(Router router) { - exchangeExpression = expression(router, language, expression); + exchangeExpression = expression(new InterceptorAdapter(router, xmlConfig), language, expression); } @Override @@ -95,4 +97,19 @@ public String getExpression() { public void setExpression(String expression) { this.expression = expression; } + + /** + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig + */ + @Override + @MCChildElement(allowForeign = true) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; + } + + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; + } } 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 9bd3c7de3d..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,16 +13,14 @@ 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.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; +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.*; import static com.predic8.membrane.core.lang.ExchangeExpression.expression; @@ -35,7 +33,7 @@ public class PolyglotSessionIdExtractor extends AbstractXmlElement implements Se public void init(Router router) { if (sessionSource != null && !sessionSource.isEmpty()) { - exchangeExpression = expression(router, language, sessionSource); + 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 f958aa8f3e..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,7 +57,7 @@ public class ForInterceptor extends AbstractFlowWithChildrenInterceptor { public void init() { super.init(); try { - exchangeExpression = expression(router, language, in); + 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 280cc1e0db..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,6 +15,7 @@ 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.lang.*; @@ -38,13 +39,14 @@ * @topic 1. Proxies and Flow */ @MCElement(name = "if") -public class IfInterceptor extends AbstractFlowWithChildrenInterceptor { +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"; @@ -53,7 +55,7 @@ public IfInterceptor() { @Override public void init() { super.init(); - exchangeExpression = expression(router, language, test); + exchangeExpression = expression(this, language, test); } @Override @@ -131,4 +133,19 @@ public String getShortDescription() { ret.append("
}"); return ret.toString(); } + + /** + * 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; + } + + @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 572610b384..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 @@ -13,31 +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.core.Router; -import com.predic8.membrane.core.exchange.Exchange; -import com.predic8.membrane.core.interceptor.Interceptor.Flow; -import com.predic8.membrane.core.lang.ExchangeExpression; -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; +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.lang.*; +import com.predic8.membrane.core.lang.ExchangeExpression.*; +import org.slf4j.*; + +import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; import static com.predic8.membrane.core.lang.ExchangeExpression.expression; @MCElement(name = "case", topLevel = false) -public class Case extends InterceptorContainer { +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 XmlConfig xmlConfig; public void init(Router router) { - exchangeExpression = expression(router, language, test); + exchangeExpression = expression( new InterceptorAdapter(router,xmlConfig), language, test); } boolean evaluate(Exchange exc, Flow flow) { @@ -77,4 +77,19 @@ public String getTest() { public void setTest(String test) { this.test = test; } + + /** + * 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; + } + + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; + } } \ 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 4d4d8ad1fc..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 @@ -14,25 +14,21 @@ 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.predic8.membrane.core.lang.ExchangeExpression; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; - -import java.util.concurrent.TimeUnit; - -import static com.predic8.membrane.core.exceptions.ProblemDetails.user; -import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; +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.*; + +import java.util.concurrent.*; + +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; -import static com.predic8.membrane.core.lang.ExchangeExpression.expression; +import static com.predic8.membrane.core.lang.ExchangeExpression.*; + /** * @description

Prevents duplicate request processing based on a dynamic idempotency key.

@@ -44,18 +40,18 @@ * @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 = expression(router, language, key); + + exchangeExpression = expression(this, language, key); processedKeys = CacheBuilder.newBuilder() .maximumSize(10000) .expireAfterWrite(expiration, TimeUnit.SECONDS) @@ -105,17 +101,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. @@ -137,10 +122,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..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 @@ -14,25 +14,23 @@ package com.predic8.membrane.core.interceptor.lang; -import com.predic8.membrane.annot.MCAttribute; -import com.predic8.membrane.core.interceptor.AbstractInterceptor; -import com.predic8.membrane.core.lang.ExchangeExpression.Language; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +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.SPEL; +import static com.predic8.membrane.core.lang.ExchangeExpression.Language.*; -abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot{ - - private static final Logger log = LoggerFactory.getLogger(AbstractLanguageInterceptor.class); +public abstract class AbstractLanguageInterceptor extends AbstractInterceptor implements Polyglot, XMLSupport { /** * SpEL is default */ protected Language language = SPEL; + protected XmlConfig xmlConfig; - public String getLanguage() { - return language.name(); + public Language getLanguage() { + return language; } /** @@ -44,4 +42,19 @@ public String getLanguage() { public void setLanguage(Language language) { this.language = language; } + + /** + * 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; + } + + @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/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/ratelimit/RateLimitInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java index 71977c7fef..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,7 +94,7 @@ protected ExchangeExpression getExchangeExpression() { // If there is no expression use the client IP if (expression.isEmpty()) return null; - return expression(router, language, expression); + return expression(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 f52f3175c2..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,7 +14,9 @@ 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.lang.groovy.*; @@ -48,12 +50,52 @@ enum Language {GROOVY, SPEL, XPATH, JSONPATH} */ T evaluate(Exchange exchange, Interceptor.Flow flow, Class type) throws ExchangeExpressionException; - static ExchangeExpression expression(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 expression(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 XMLSupport { + + private XmlConfig xmlConfig; + + public InterceptorAdapter(Router router) { + this.router = router; + } + + public InterceptorAdapter(Router router, XmlConfig xmlConfig) { + this.router = router; + this.xmlConfig = xmlConfig; + } + + /** + * 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; + } + + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; + } + } } 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 1240c5840c..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 @@ -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.*; @@ -37,17 +38,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 @@ -84,7 +85,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<>(); @@ -96,7 +97,7 @@ protected static List parseTokens(Router router, Language language, Strin } String expr = m.group(3); if (expr != null) { - tokens.add(new Expression(expression(router, language, expr))); + tokens.add(new Expression(expression(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..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 @@ -14,28 +14,38 @@ 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.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 final XmlParser parser = HardenedXmlParser.getInstance(); + + private XmlConfig xmlConfig; + + // 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 XMLSupport xns) { + xmlConfig = xns.getXmlConfig(); + } } @Override @@ -72,7 +82,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 (xmlConfig != null && xmlConfig.getNamespaces() != null) { + xPath.setNamespaceContext(xmlConfig.getNamespaces().getNamespaceContext()); + } + + return xPath.evaluate(expression, parser.parse(XMLUtil.getInputSource(msg)), xmlType); + } + + 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 3e6a743508..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,13 +20,16 @@ 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.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.*; import com.predic8.membrane.core.openapi.util.UriUtil; 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,6 +61,7 @@ public class APIProxy extends ServiceProxy implements Polyglot { private String test; private String id; private ApiDescription description; + private XmlConfig xmlConfig; protected Map apiRecords = new LinkedHashMap<>(); @@ -83,7 +87,7 @@ public void setSpecs(List specs) { public void init() { super.init(); if (test != null && !test.isEmpty()) { - exchangeExpression = expression(router, language, test); + exchangeExpression = expression(new InterceptorAdapter(router, xmlConfig), language, test); } key = new APIProxyKey(key, exchangeExpression, !specs.isEmpty()); initOpenAPI(); @@ -236,4 +240,18 @@ public String getContent() { return content; } } + + /** + * XML Configuration e.g. declaration of XML namespaces for XPath expressions, ... + * @param xmlConfig + */ + @MCChildElement(allowForeign = true,order = 10) + public void setXmlConfig(XmlConfig xmlConfig) { + this.xmlConfig = xmlConfig; + } + + 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 858f800111..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,9 +18,11 @@ 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.Interceptor; +import com.predic8.membrane.core.interceptor.*; 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.transport.ssl.*; @@ -96,7 +98,7 @@ public void setPath(Path path) { *

*/ @MCElement(name = "target", topLevel = false) - public static class Target { + public static class Target implements XMLSupport { private String host; private int port = -1; private String method; @@ -107,12 +109,17 @@ public static class Target { private SSLParser sslParser; + protected XmlConfig xmlConfig; + public void init(Router router) { - if (url != null) exchangeExpression = TemplateExchangeExpression.newInstance(router, language, url); + if (url != null) { + exchangeExpression = TemplateExchangeExpression.newInstance(new InterceptorAdapter(router,xmlConfig), language, url); + } + } 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 */ @@ -228,6 +235,21 @@ public ExchangeExpression.Language getLanguage() { public void setLanguage(ExchangeExpression.Language language) { this.language = language; } + + /** + * 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; + } + + @Override + public XmlConfig getXmlConfig() { + return xmlConfig; + } } 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..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 @@ -14,6 +14,7 @@ package com.predic8.membrane.core.util; public class ExceptionUtil { + public static String concatMessageAndCauseMessages(Throwable throwable) { StringBuilder sb = new StringBuilder(); do { @@ -25,4 +26,14 @@ public static String concatMessageAndCauseMessages(Throwable throwable) { } while (throwable != null); return sb.toString(); } + + public static Throwable getRootCause(Throwable t) { + if (t == null) + return null; + 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/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/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..51313588ff --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/xml/parser/HardenedXmlParser.java @@ -0,0 +1,100 @@ +/* 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(); + + /** + * 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() {} + + public static XmlParser getInstance() { + if (INSTANCE == null) { + // 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; + } + + private static DocumentBuilderFactory createFactory() { + DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); + f.setNamespaceAware(true); + + 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; + } + + /** + * Creates a new DocumentBuilder for XML parsing. Access is not thread-safe! + * @return + */ + 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..232e27576a --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/lang/NamespacesTest.java @@ -0,0 +1,43 @@ +/* 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.config.xml.*; +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 ab47f78167..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,14 +66,14 @@ static void tearDown() { protected abstract Language getLanguage(); protected Object evalObject(String expression) { - return expression(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 expression(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 expression(router, getLanguage(),expression).evaluate(exchange,flow, String.class); + 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/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 07ef377e48..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,24 +117,23 @@ void emptyBodyForBoolean() throws URISyntaxException { @Test void wrongContentType() throws URISyntaxException { - assertEquals("", expression(router, JSONPATH, "$") - .evaluate(post("/foo").contentType(TEXT_XML).buildExchange(), REQUEST, String.class)); + assertEquals("", expression(new InterceptorAdapter(router), JSONPATH, "$") + .evaluate(Request.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, "$") - .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/SetPropertyInterceptorXPathTest.java b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java new file mode 100644 index 0000000000..915149fb6e --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/lang/xpath/SetPropertyInterceptorXPathTest.java @@ -0,0 +1,129 @@ +/* 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.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.*; + +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 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() { + 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(); + XmlConfig xc = new XmlConfig(); + xc.setNamespaces(namespaces); + i.setXmlConfig(xc); + 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/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..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 @@ -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.expression; 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", expression( 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/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/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/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/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..ee5c2e1395 --- /dev/null +++ b/distribution/examples/xml/namespaces/README.md @@ -0,0 +1,27 @@ +# 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 declarations 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.* + +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: + ```bash + 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..519c3ece52 --- /dev/null +++ b/distribution/examples/xml/namespaces/proxies.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + /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..e9e79af326 --- /dev/null +++ b/distribution/examples/xml/namespaces/requests.http @@ -0,0 +1,9 @@ +POST http://localhost:2000 +Content-Type: text/xml + + + Herbert + + 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..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 @@ -19,6 +19,7 @@ import java.io.*; import static io.restassured.RestAssured.*; +import static io.restassured.filter.log.LogDetail.*; import static org.hamcrest.Matchers.*; public class VersioningSoapXsltExampleTest extends DistributionExtractingTestcase { @@ -53,6 +54,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..8f6f5bb6a1 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/xml/namespaces/NamespacesExampleTest.java @@ -0,0 +1,172 @@ +/* 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 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 { + + @Override + protected String getExampleDirName() { + return "xml/namespaces"; + } + + @Test + void namespaceAwareXPathExtraction() { + 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() { + 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 + } + + @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 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 + } + + @Test + void headerIdIsSet() { + String xmlBody = """ + + Hans + + Cologne + + + """; + + // @formatter:off + given() + .contentType(TEXT_XML) + .body(xmlBody) + .post("http://localhost:2000") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .header("x-seen-request-id", equalTo("77")) + .body(containsString("Hans from Cologne")); + // @formatter:on + } + +} 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).