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
*/
@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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${property.name} from ${property.city}
+
+
+
+
+
+
+
+
+
\ 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).