From b7666ee879ac6be606272e9b8370e015c92b497c Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 12:00:18 +0100
Subject: [PATCH 1/7] refactor(core): move `StringUtil` to `text` package,
extend XSLT transformation support, and add tutorial examples
---
.../AbstractTemplateInterceptor.java | 2 +-
.../templating/TemplateInterceptor.java | 2 +-
.../interceptor/xml/Json2XmlInterceptor.java | 2 +-
.../interceptor/xslt/XSLTInterceptor.java | 167 ++++++++++--------
.../jsonpath/JsonpathExchangeExpression.java | 2 +-
.../core/openapi/serviceproxy/APIProxy.java | 2 +-
.../transport/http/HttpServerHandler.java | 4 +-
.../membrane/core/util/text/StringUtil.java | 2 +-
.../interceptor/xslt/XSLTInterceptorTest.java | 159 +++++++++--------
.../membrane/core/util/StringUtilTest.java | 3 +-
...sltJson2XMLTransformationTutorialTest.java | 32 ++++
.../xml/35-XSLT-transformation-to-json.yaml | 26 +++
...yaml => 40-XSLT-transformation-group.yaml} | 6 +-
distribution/tutorials/xml/to-json.xsl | 22 +++
docs/ROADMAP.md | 5 +-
15 files changed, 274 insertions(+), 162 deletions(-)
create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java
create mode 100644 distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml
rename distribution/tutorials/xml/{40-XSLT-transformation.yaml => 40-XSLT-transformation-group.yaml} (58%)
create mode 100644 distribution/tutorials/xml/to-json.xsl
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/AbstractTemplateInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/AbstractTemplateInterceptor.java
index 040078f7b4..dc9d46b1e7 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/AbstractTemplateInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/AbstractTemplateInterceptor.java
@@ -31,7 +31,7 @@
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static com.predic8.membrane.core.resolver.ResolverMap.*;
-import static com.predic8.membrane.core.util.StringUtil.*;
+import static com.predic8.membrane.core.util.text.StringUtil.*;
import static java.nio.charset.StandardCharsets.*;
import static org.apache.commons.text.StringEscapeUtils.*;
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java
index 266b586ad3..74fa39742d 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/templating/TemplateInterceptor.java
@@ -31,7 +31,7 @@
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static com.predic8.membrane.core.lang.ScriptingUtils.*;
import static com.predic8.membrane.core.util.FileUtil.*;
-import static com.predic8.membrane.core.util.StringUtil.addLineNumbers;
+import static com.predic8.membrane.core.util.text.StringUtil.addLineNumbers;
import static java.nio.charset.StandardCharsets.*;
/**
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptor.java
index 9f633bfb28..7337dd3ff4 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptor.java
@@ -24,7 +24,7 @@
import static com.predic8.membrane.core.http.MimeType.*;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*;
import static com.predic8.membrane.core.interceptor.Outcome.*;
-import static com.predic8.membrane.core.util.StringUtil.*;
+import static com.predic8.membrane.core.util.text.StringUtil.*;
import static java.nio.charset.StandardCharsets.*;
/**
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
index 1697b64c40..634922ad49 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
@@ -19,110 +19,123 @@
import com.predic8.membrane.core.interceptor.*;
import com.predic8.membrane.core.multipart.*;
import com.predic8.membrane.core.util.*;
-import com.predic8.membrane.core.util.text.*;
+import org.jetbrains.annotations.*;
import org.slf4j.*;
+import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.util.*;
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.util.text.TextUtil.*;
/**
* @description
- * The transform feature applies an XSLT transformation to the content in the body of a message. After the
- * transformation the body content is replaced with the result of the transformation.
- *
+ * The transform feature applies an XSLT transformation to the content in the body of a message. After the
+ * transformation the body content is replaced with the result of the transformation.
+ *
* @topic 2. Enterprise Integration Patterns
*/
-@MCElement(name="transform")
+@MCElement(name = "transform")
public class XSLTInterceptor extends AbstractInterceptor {
- private static final Logger log = LoggerFactory.getLogger(XSLTInterceptor.class.getName());
+ private static final Logger log = LoggerFactory.getLogger(XSLTInterceptor.class.getName());
- private String xslt;
- private volatile XSLTTransformer xsltTransformer;
- private final XOPReconstitutor xopr = new XOPReconstitutor();
+ private String xslt;
+ private volatile XSLTTransformer xsltTransformer;
+ private final XOPReconstitutor xopr = new XOPReconstitutor();
- public XSLTInterceptor() {
- name = "xslt transformer";
- }
+ public XSLTInterceptor() {
+ name = "xslt transformer";
+ }
- @Override
- public Outcome handleRequest(Exchange exc) {
- try {
- transformMsg(exc.getRequest(), xslt, exc.getStringProperties());
- } catch (Exception e) {
- user(router.getConfiguration().isProduction(),getDisplayName())
- .detail("Error transforming request!")
- .exception(e)
- .buildAndSetResponse(exc);
- return ABORT;
- }
- return CONTINUE;
- }
+ @Override
+ public Outcome handleRequest(Exchange exc) {
+ return handleInternal(exc, REQUEST);
+ }
+
+ @Override
+ public Outcome handleResponse(Exchange exc) {
+ return handleInternal(exc, RESPONSE);
+ }
+
+ private Outcome handleInternal(Exchange exc, Flow flow) {
+ var msg = exc.getMessage(flow);
- @Override
- public Outcome handleResponse(Exchange exc) {
try {
- transformMsg(exc.getResponse(), xslt, exc.getStringProperties());
+ transformMsg(msg, xslt, exc.getStringProperties());
+ } catch (TransformerException e) {
+ log.debug("", e);
+ if (e.getMessage().contains("not allowed in prolog")) {
+ user(router.getConfiguration().isProduction(), getDisplayName())
+ .title("Content not allowed in prolog of XML input.")
+ .detail("Check for extra characters before the XML declaration ")
+ .internal("offendingInput", com.predic8.membrane.core.util.text.StringUtil.truncateAfter(msg.getBodyAsStringDecoded() + "...", 50))
+ .buildAndSetResponse(exc);
+ return ABORT;
+ }
+ return createErrorResponse(exc, e);
} catch (Exception e) {
- log.error("Error transforming response!", e);
- user(router.getConfiguration().isProduction(),getDisplayName())
- .detail("Error transforming response!")
- .exception(e)
- .buildAndSetResponse(exc);
- return ABORT;
+ log.info("", e);
+ return createErrorResponse(exc, e);
}
return CONTINUE;
- }
-
- private void transformMsg(Message msg, String ss, Map parameter) throws Exception {
- if (msg.isBodyEmpty())
- return;
- msg.setBodyContent(xsltTransformer.transform(
- new StreamSource(xopr.reconstituteIfNecessary(msg)), parameter));
- }
-
- @Override
- public void init() {
- super.init();
+ }
+
+ private @NotNull Outcome createErrorResponse(Exchange exc, Exception e) {
+ user(router.getConfiguration().isProduction(), getDisplayName())
+ .detail("Error transforming request!")
+ .exception(e)
+ .buildAndSetResponse(exc);
+ return ABORT;
+ }
+
+ private void transformMsg(Message msg, String ss, Map parameter) throws Exception {
+ if (msg.isBodyEmpty())
+ return;
+ msg.setBodyContent(xsltTransformer.transform(
+ new StreamSource(xopr.reconstituteIfNecessary(msg)), parameter));
+ }
+
+ @Override
+ public void init() {
+ super.init();
try {
xsltTransformer = new XSLTTransformer(xslt, router, getConcurrency());
} catch (Exception e) {
- throw new ConfigurationException("Could not create XSLT transformer",e);
+ throw new ConfigurationException("Could not create XSLT transformer", e);
}
}
- private static int getConcurrency() {
- return Runtime.getRuntime().availableProcessors() * 2;
- }
-
- public String getXslt() {
- return xslt;
- }
-
- /**
- * @description Location of the XSLT stylesheet that will be applied to request and response.
- * @example strip.xslt
- */
- @MCAttribute
- public void setXslt(String xslt) {
- this.xslt = xslt;
- this.xsltTransformer = null;
- }
-
- @Override
- public String getShortDescription() {
- return "Applies an XSLT transformation.";
- }
-
- @Override
- public String getLongDescription() {
- return TextUtil.removeFinalChar(getShortDescription()) +
- " using the stylesheet at " +
- TextUtil.linkURL(xslt) +
- " .";
- }
+ private static int getConcurrency() {
+ return Runtime.getRuntime().availableProcessors() * 2;
+ }
+
+ public String getXslt() {
+ return xslt;
+ }
+
+ /**
+ * @description Location of the XSLT stylesheet that will be applied to request and response.
+ * @example strip.xslt
+ */
+ @MCAttribute
+ public void setXslt(String xslt) {
+ this.xslt = xslt;
+ this.xsltTransformer = null;
+ }
+
+ @Override
+ public String getShortDescription() {
+ return "Applies an XSLT transformation.";
+ }
+
+ @Override
+ public String getLongDescription() {
+ return "%s using the stylesheet at %s .".formatted(removeFinalChar(getShortDescription()), linkURL(xslt));
+ }
}
diff --git a/core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java b/core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java
index 1f89704390..a4d22a1fcf 100644
--- a/core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java
+++ b/core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java
@@ -28,7 +28,7 @@
import java.io.*;
import java.util.*;
-import static com.predic8.membrane.core.util.StringUtil.*;
+import static com.predic8.membrane.core.util.text.StringUtil.*;
import static java.lang.Boolean.*;
import static java.nio.charset.StandardCharsets.*;
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 6fbcc93afd..46fdc90812 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
@@ -38,7 +38,7 @@
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.util.StringUtil.maskNonPrintableCharacters;
+import static com.predic8.membrane.core.util.text.StringUtil.maskNonPrintableCharacters;
/**
* @description The api proxy extends the serviceProxy with API related functions like OpenAPI support and path parameters.
diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpServerHandler.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpServerHandler.java
index b12139d834..63cac2f309 100644
--- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpServerHandler.java
+++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpServerHandler.java
@@ -35,8 +35,8 @@
import static com.predic8.membrane.core.transport.http.ByteStreamLogging.wrapConnectionOutputStream;
import static com.predic8.membrane.core.transport.http.HttpServerHandler.RequestProcessingResult.*;
import static com.predic8.membrane.core.transport.http.HttpServerThreadFactory.DEFAULT_THREAD_NAME;
-import static com.predic8.membrane.core.util.StringUtil.maskNonPrintableCharacters;
-import static com.predic8.membrane.core.util.StringUtil.truncateAfter;
+import static com.predic8.membrane.core.util.text.StringUtil.maskNonPrintableCharacters;
+import static com.predic8.membrane.core.util.text.StringUtil.truncateAfter;
import static java.lang.Thread.currentThread;
public class HttpServerHandler extends AbstractHttpHandler implements Runnable, TwoWayStreaming {
diff --git a/core/src/main/java/com/predic8/membrane/core/util/text/StringUtil.java b/core/src/main/java/com/predic8/membrane/core/util/text/StringUtil.java
index 326a10a9d6..3b80e88a9b 100644
--- a/core/src/main/java/com/predic8/membrane/core/util/text/StringUtil.java
+++ b/core/src/main/java/com/predic8/membrane/core/util/text/StringUtil.java
@@ -12,7 +12,7 @@
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.text;
import java.util.*;
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
index 32011d695f..e3b0feb0e7 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
@@ -13,79 +13,96 @@
limitations under the License. */
package com.predic8.membrane.core.interceptor.xslt;
-import java.io.InputStream;
-
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
-import org.junit.jupiter.api.Test;
-import org.xml.sax.InputSource;
-
-import com.predic8.membrane.core.router.DummyTestRouter;
-import com.predic8.membrane.core.exchange.Exchange;
-import com.predic8.membrane.core.http.Response;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import com.predic8.membrane.core.exchange.*;
+import com.predic8.membrane.core.http.*;
+import com.predic8.membrane.core.interceptor.*;
+import com.predic8.membrane.core.router.*;
+import org.hamcrest.*;
+import org.junit.jupiter.api.*;
+import org.xml.sax.*;
+
+import javax.xml.xpath.*;
+import java.io.*;
+import java.net.*;
+
+import static com.predic8.membrane.core.http.Request.get;
+import static com.predic8.membrane.core.http.Response.*;
+import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
+import static org.junit.jupiter.api.Assertions.*;
public class XSLTInterceptorTest {
- Exchange exc = new Exchange(null);
- final XPath xpath = XPathFactory.newInstance().newXPath();
-
- @Test
- public void testRequest() throws Exception {
- exc = new Exchange(null);
- exc.setResponse(Response.ok().body(getClass().getResourceAsStream("/customer.xml"), true).build());
-
- XSLTInterceptor i = new XSLTInterceptor();
- i.setXslt("classpath:/customer2person.xsl");
- i.init(new DummyTestRouter());
- i.handleResponse(exc);
-
- //printBodyContent();
- assertXPath("/person/name/first", "Rick");
- assertXPath("/person/name/last", "Cort\u00e9s Ribotta");
- assertXPath("/person/address/street",
- "Calle P\u00fablica \"B\" 5240 Casa 121");
- assertXPath("/person/address/city", "Omaha");
- }
-
- @Test
- public void testXSLTParameter() throws Exception {
- exc = new Exchange(null);
- exc.setResponse(Response.ok().body(getClass().getResourceAsStream("/customer.xml"), true).build());
-
- exc.setProperty("XSLT_COMPANY", "predic8");
-
- XSLTInterceptor i = new XSLTInterceptor();
- i.setXslt("classpath:/customer2personAddCompany.xsl");
- i.init(new DummyTestRouter());
- i.handleResponse(exc);
-
- //printBodyContent();
- assertXPath("/person/name/first", "Rick");
- assertXPath("/person/name/last", "Cort\u00e9s Ribotta");
- assertXPath("/person/address/street",
- "Calle P\u00fablica \"B\" 5240 Casa 121");
- assertXPath("/person/address/city", "Omaha");
- assertXPath("/person/company", "predic8");
- }
-
- @SuppressWarnings("unused")
- private void printBodyContent() throws Exception {
- InputStream i = exc.getResponse().getBodyAsStream();
- int read = 0;
- byte[] buf = new byte[4096];
- while ((read = i.read(buf)) != -1) {
- System.out.write(buf, 0, read);
- }
- }
-
- private void assertXPath(String xpathExpr, String expected)
- throws XPathExpressionException {
- assertEquals(expected, xpath.evaluate(xpathExpr, new InputSource(exc
- .getResponse().getBodyAsStream())));
- }
+ Exchange exc = new Exchange(null);
+ final XPath xpath = XPathFactory.newInstance().newXPath();
+
+ @Test
+ void testRequest() throws Exception {
+ exc = new Exchange(null);
+ exc.setResponse(ok().body(getClass().getResourceAsStream("/customer.xml"), true).build());
+
+ XSLTInterceptor i = new XSLTInterceptor();
+ i.setXslt("classpath:/customer2person.xsl");
+ i.init(new DummyTestRouter());
+ i.handleResponse(exc);
+
+ //printBodyContent();
+ assertXPath("/person/name/first", "Rick");
+ assertXPath("/person/name/last", "Cort\u00e9s Ribotta");
+ assertXPath("/person/address/street",
+ "Calle P\u00fablica \"B\" 5240 Casa 121");
+ assertXPath("/person/address/city", "Omaha");
+ }
+
+ @Test
+ void testXSLTParameter() throws Exception {
+ exc = new Exchange(null);
+ exc.setResponse(ok().body(getClass().getResourceAsStream("/customer.xml"), true).build());
+
+ exc.setProperty("XSLT_COMPANY", "predic8");
+
+ XSLTInterceptor i = new XSLTInterceptor();
+ i.setXslt("classpath:/customer2personAddCompany.xsl");
+ i.init(new DummyTestRouter());
+ i.handleResponse(exc);
+
+ //printBodyContent();
+ assertXPath("/person/name/first", "Rick");
+ assertXPath("/person/name/last", "Cort\u00e9s Ribotta");
+ assertXPath("/person/address/street",
+ "Calle P\u00fablica \"B\" 5240 Casa 121");
+ assertXPath("/person/address/city", "Omaha");
+ assertXPath("/person/company", "predic8");
+ }
+
+ @Test
+ void noConentInProlog() throws Exception {
+ exc = get("http://localhost/").body("rubbish").buildExchange();
+
+ var i = new XSLTInterceptor();
+ i.setXslt("classpath:/customer2personAddCompany.xsl");
+ i.init(new DummyTestRouter());
+ assertEquals(ABORT, i.handleRequest(exc));
+ assertEquals(400, exc.getResponse().getStatusCode());
+ String body = exc.getResponse().getBodyAsStringDecoded();
+ System.out.println(body);
+ assertTrue(body.contains("rubbish"));
+ assertTrue(body.contains("not allowed in prolog"));
+ }
+
+ @SuppressWarnings("unused")
+ private void printBodyContent() throws Exception {
+ InputStream i = exc.getResponse().getBodyAsStream();
+ int read = 0;
+ byte[] buf = new byte[4096];
+ while ((read = i.read(buf)) != -1) {
+ System.out.write(buf, 0, read);
+ }
+ }
+
+ private void assertXPath(String xpathExpr, String expected)
+ throws XPathExpressionException {
+ assertEquals(expected, xpath.evaluate(xpathExpr, new InputSource(exc
+ .getResponse().getBodyAsStream())));
+ }
}
diff --git a/core/src/test/java/com/predic8/membrane/core/util/StringUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/StringUtilTest.java
index bad24a0096..a39b1ca724 100644
--- a/core/src/test/java/com/predic8/membrane/core/util/StringUtilTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/util/StringUtilTest.java
@@ -18,8 +18,7 @@
import java.util.*;
-import static com.predic8.membrane.core.util.StringUtil.*;
-import static com.predic8.membrane.core.util.StringUtil.splitByComma;
+import static com.predic8.membrane.core.util.text.StringUtil.*;
import static org.junit.jupiter.api.Assertions.*;
class StringUtilTest {
diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java
new file mode 100644
index 0000000000..2bbe60a3a9
--- /dev/null
+++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java
@@ -0,0 +1,32 @@
+package com.predic8.membrane.tutorials.xml;
+
+import org.junit.jupiter.api.*;
+
+import java.io.*;
+
+import static io.restassured.RestAssured.*;
+import static io.restassured.http.ContentType.*;
+import static org.hamcrest.Matchers.*;
+
+public class XsltJson2XMLTransformationTutorialTest extends AbstractXmlTutorialTest{
+ @Override
+ protected String getTutorialYaml() {
+ return "35-XSLT-Transformation-to-json.yaml";
+ }
+
+ @Test
+ void xsltTransformsXml() throws IOException {
+ // @formatter:off
+ given()
+ .body(readFileFromBaseDir("books.xml"))
+ .contentType(XML)
+ .when()
+ .post("http://localhost:2000")
+ .then()
+ .statusCode(200)
+ .contentType(JSON)
+ .body("books.size()", greaterThan(0))
+ .body("books[0].year", equalTo(1975));
+ // @formatter:on
+ }
+}
diff --git a/distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml b/distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml
new file mode 100644
index 0000000000..90b2d58096
--- /dev/null
+++ b/distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml
@@ -0,0 +1,26 @@
+# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.6.json
+#
+# Tutorial: XSLT Transformation from XML to JSON
+#
+# The `transform` plugin uses a stylesheet to transform XML into a
+# different text format. One common use case is to transform XML into JSON.
+# The templates in the `to-json.xsl` stylesheet could be used as a starting point
+# for own XML to JSON transformations.
+#
+# Try:
+# curl -d @books.xml -H "Content-Type: text/xml" localhost:2000
+
+api:
+ port: 2000
+ flow:
+ - request:
+ - transform:
+ # The stylesheet to use for the transformation
+ xslt: to-json.xsl
+ - setHeader:
+ name: Content-Type
+ value: application/json
+ # The beautifier needs the right Content-Type to format the output of the transform
+ - beautifier: {}
+ - return:
+ status: 200
diff --git a/distribution/tutorials/xml/40-XSLT-transformation.yaml b/distribution/tutorials/xml/40-XSLT-transformation-group.yaml
similarity index 58%
rename from distribution/tutorials/xml/40-XSLT-transformation.yaml
rename to distribution/tutorials/xml/40-XSLT-transformation-group.yaml
index eed4e74c06..8d0e823ee4 100644
--- a/distribution/tutorials/xml/40-XSLT-transformation.yaml
+++ b/distribution/tutorials/xml/40-XSLT-transformation-group.yaml
@@ -1,11 +1,11 @@
# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.6.json
#
-# Tutorial: XSLT Transformation
+# Tutorial: XSLT Transformation Grouping
#
-# Use an XSLT stylesheet to transform XML.
+# Complex XSLT transformation that regroups input
#
# Try:
-# curl -d @books.xml localhost:2000
+# curl -d @books.xml -H "Content-Type: text/xml" localhost:2000
api:
port: 2000
diff --git a/distribution/tutorials/xml/to-json.xsl b/distribution/tutorials/xml/to-json.xsl
new file mode 100644
index 0000000000..c5a0dd90a8
--- /dev/null
+++ b/distribution/tutorials/xml/to-json.xsl
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ { "books": [ ] }
+
+
+
+ { }
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 2195f5195c..07fe8f8176 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -8,6 +8,9 @@
PRIO 1:
+- Reverse:
+ - First parse
+ - Second validate YAML
- Tutorials:
- Add how to run the tutorials in a Docker container
- HotReload for YAML
@@ -17,7 +20,7 @@ PRIO 1:
- if: Add hint in documentation: use choice otherwise for else TB
- Register JSON Schema for YAML at: https://www.schemastore.org TB
- create test asserting that connection reuse via proxy works TP
-- Central description of Membrane Languages, Cheat Sheets, links to their docs.
+- Central description of Membrane Languages, Cheat Sheets, links to their docs. TP
- Central description of MEMBRANE_* environment variables
- Like MEMBRANE_HOME...
- @coderabbitai look through the code base for usages of these variables and suggest documentation
From 4d912a1cb10836a5d21905dfdce91ea84218f8f1 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 13:15:56 +0100
Subject: [PATCH 2/7] refactor(tutorials): improve XML to JSON XSLT
transformation readability and output consistency
---
distribution/tutorials/xml/to-json.xsl | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/distribution/tutorials/xml/to-json.xsl b/distribution/tutorials/xml/to-json.xsl
index c5a0dd90a8..156fe22242 100644
--- a/distribution/tutorials/xml/to-json.xsl
+++ b/distribution/tutorials/xml/to-json.xsl
@@ -2,20 +2,28 @@
-
+
+
- { "books": [ ] }
+ { "books": [
+
+ ] }
+
- { }
+ {
+
+ }
+
+ ,
-
-
-
-
+
+
+ "": ""
+ ,
From 8fe7cee82fd952cd60aedb4dd87142a81186ca08 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 13:20:57 +0100
Subject: [PATCH 3/7] refactor(xslt): rename test class and improve error
handling in XSLT transformations
---
.../core/interceptor/xslt/XSLTInterceptor.java | 14 ++++++++------
...=> XsltXML2JSONTransformationTutorialTest.java} | 2 +-
2 files changed, 9 insertions(+), 7 deletions(-)
rename distribution/src/test/java/com/predic8/membrane/tutorials/xml/{XsltJson2XMLTransformationTutorialTest.java => XsltXML2JSONTransformationTutorialTest.java} (92%)
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
index 634922ad49..0c76b0ab35 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
@@ -30,6 +30,7 @@
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.util.text.StringUtil.*;
import static com.predic8.membrane.core.util.text.TextUtil.*;
/**
@@ -69,26 +70,27 @@ private Outcome handleInternal(Exchange exc, Flow flow) {
transformMsg(msg, xslt, exc.getStringProperties());
} catch (TransformerException e) {
log.debug("", e);
- if (e.getMessage().contains("not allowed in prolog")) {
+ if (e.getMessage() != null && e.getMessage().contains("not allowed in prolog")) {
user(router.getConfiguration().isProduction(), getDisplayName())
.title("Content not allowed in prolog of XML input.")
.detail("Check for extra characters before the XML declaration ")
- .internal("offendingInput", com.predic8.membrane.core.util.text.StringUtil.truncateAfter(msg.getBodyAsStringDecoded() + "...", 50))
+ .internal("offendingInput", truncateAfter(msg.getBodyAsStringDecoded() + "...", 50))
.buildAndSetResponse(exc);
return ABORT;
}
- return createErrorResponse(exc, e);
+ return createErrorResponse(exc,e,flow);
} catch (Exception e) {
log.info("", e);
- return createErrorResponse(exc, e);
+ return createErrorResponse(exc,e,flow);
}
return CONTINUE;
}
- private @NotNull Outcome createErrorResponse(Exchange exc, Exception e) {
+ private @NotNull Outcome createErrorResponse(Exchange exc, Exception e, Flow flow) {
user(router.getConfiguration().isProduction(), getDisplayName())
- .detail("Error transforming request!")
+ .detail("Error transforming message!")
.exception(e)
+ .internal("flow", flow.toString())
.buildAndSetResponse(exc);
return ABORT;
}
diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
similarity index 92%
rename from distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java
rename to distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
index 2bbe60a3a9..9443dfffbd 100644
--- a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltJson2XMLTransformationTutorialTest.java
+++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
@@ -8,7 +8,7 @@
import static io.restassured.http.ContentType.*;
import static org.hamcrest.Matchers.*;
-public class XsltJson2XMLTransformationTutorialTest extends AbstractXmlTutorialTest{
+public class XsltXML2JSONTransformationTutorialTest extends AbstractXmlTutorialTest{
@Override
protected String getTutorialYaml() {
return "35-XSLT-Transformation-to-json.yaml";
From 1daf774bca461cb4fa4650d42a5256c431ed68bc Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 13:22:15 +0100
Subject: [PATCH 4/7] refactor(xslt): simplify `transformMsg` method and clean
up test code
---
.../membrane/core/interceptor/xslt/XSLTInterceptor.java | 4 ++--
.../membrane/core/interceptor/xslt/XSLTInterceptorTest.java | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
index 0c76b0ab35..e666dc5e17 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptor.java
@@ -67,7 +67,7 @@ private Outcome handleInternal(Exchange exc, Flow flow) {
var msg = exc.getMessage(flow);
try {
- transformMsg(msg, xslt, exc.getStringProperties());
+ transformMsg(msg, exc.getStringProperties());
} catch (TransformerException e) {
log.debug("", e);
if (e.getMessage() != null && e.getMessage().contains("not allowed in prolog")) {
@@ -95,7 +95,7 @@ private Outcome handleInternal(Exchange exc, Flow flow) {
return ABORT;
}
- private void transformMsg(Message msg, String ss, Map parameter) throws Exception {
+ private void transformMsg(Message msg, Map parameter) throws Exception {
if (msg.isBodyEmpty())
return;
msg.setBodyContent(xsltTransformer.transform(
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
index e3b0feb0e7..aa01e424c6 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
@@ -92,7 +92,7 @@ void noConentInProlog() throws Exception {
@SuppressWarnings("unused")
private void printBodyContent() throws Exception {
InputStream i = exc.getResponse().getBodyAsStream();
- int read = 0;
+ int read;
byte[] buf = new byte[4096];
while ((read = i.read(buf)) != -1) {
System.out.write(buf, 0, read);
From 3bce831491f505be8146aacaeff5006e4d8efa2b Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 13:32:48 +0100
Subject: [PATCH 5/7] refactor(xslt): fix case sensitivity in YAML file
reference and remove unused test code
---
.../core/interceptor/xslt/XSLTInterceptorTest.java | 12 ------------
.../xml/XsltTransformationTutorialTest.java | 2 +-
...json.yaml => 35-XSLT-Transformation-to-json.yaml} | 0
...-group.yaml => 40-XSLT-Transformation-group.yaml} | 0
4 files changed, 1 insertion(+), 13 deletions(-)
rename distribution/tutorials/xml/{35-XSLT-transformation-to-json.yaml => 35-XSLT-Transformation-to-json.yaml} (100%)
rename distribution/tutorials/xml/{40-XSLT-transformation-group.yaml => 40-XSLT-Transformation-group.yaml} (100%)
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
index aa01e424c6..94dc4b3c7f 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
@@ -84,25 +84,13 @@ void noConentInProlog() throws Exception {
assertEquals(ABORT, i.handleRequest(exc));
assertEquals(400, exc.getResponse().getStatusCode());
String body = exc.getResponse().getBodyAsStringDecoded();
- System.out.println(body);
assertTrue(body.contains("rubbish"));
assertTrue(body.contains("not allowed in prolog"));
}
- @SuppressWarnings("unused")
- private void printBodyContent() throws Exception {
- InputStream i = exc.getResponse().getBodyAsStream();
- int read;
- byte[] buf = new byte[4096];
- while ((read = i.read(buf)) != -1) {
- System.out.write(buf, 0, read);
- }
- }
-
private void assertXPath(String xpathExpr, String expected)
throws XPathExpressionException {
assertEquals(expected, xpath.evaluate(xpathExpr, new InputSource(exc
.getResponse().getBodyAsStream())));
}
-
}
diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
index b8fe0d56d6..48c908ef45 100644
--- a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
+++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
@@ -11,7 +11,7 @@
public class XsltTransformationTutorialTest extends AbstractXmlTutorialTest{
@Override
protected String getTutorialYaml() {
- return "40-XSLT-transformation.yaml";
+ return "40-XSLT-Transformation.yaml";
}
@Test
diff --git a/distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml b/distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml
similarity index 100%
rename from distribution/tutorials/xml/35-XSLT-transformation-to-json.yaml
rename to distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml
diff --git a/distribution/tutorials/xml/40-XSLT-transformation-group.yaml b/distribution/tutorials/xml/40-XSLT-Transformation-group.yaml
similarity index 100%
rename from distribution/tutorials/xml/40-XSLT-transformation-group.yaml
rename to distribution/tutorials/xml/40-XSLT-Transformation-group.yaml
From 5115d9426d7672302e6c85cc3c25dd64b5baaec1 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Mon, 19 Jan 2026 15:23:53 +0100
Subject: [PATCH 6/7] refactor(xslt): update YAML file references, fix typo in
test method, and clarify beautifier comment
---
.../membrane/core/interceptor/xslt/XSLTInterceptorTest.java | 2 +-
.../membrane/tutorials/xml/XsltTransformationTutorialTest.java | 2 +-
distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
index 94dc4b3c7f..29f2c1772e 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/xslt/XSLTInterceptorTest.java
@@ -75,7 +75,7 @@ void testXSLTParameter() throws Exception {
}
@Test
- void noConentInProlog() throws Exception {
+ void noContentInProlog() throws Exception {
exc = get("http://localhost/").body("rubbish").buildExchange();
var i = new XSLTInterceptor();
diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
index 48c908ef45..46295686ef 100644
--- a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
+++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java
@@ -11,7 +11,7 @@
public class XsltTransformationTutorialTest extends AbstractXmlTutorialTest{
@Override
protected String getTutorialYaml() {
- return "40-XSLT-Transformation.yaml";
+ return "40-XSLT-Transformation-group.yaml";
}
@Test
diff --git a/distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml b/distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml
index 90b2d58096..54e47b4e03 100644
--- a/distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml
+++ b/distribution/tutorials/xml/35-XSLT-Transformation-to-json.yaml
@@ -20,7 +20,7 @@ api:
- setHeader:
name: Content-Type
value: application/json
- # The beautifier needs the right Content-Type to format the output of the transform
+ # The beautifier needs the Content-Type to format the output as JSON
- beautifier: {}
- return:
status: 200
From c0f90571743725961d8cd548aa12908e740f6b35 Mon Sep 17 00:00:00 2001
From: Thomas Bayer
Date: Tue, 20 Jan 2026 09:02:50 +0100
Subject: [PATCH 7/7] refactor(tutorials): change year type in XML to JSON
transformation test to string for consistency
---
.../tutorials/xml/XsltXML2JSONTransformationTutorialTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
index 9443dfffbd..9bfcc3fbba 100644
--- a/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
+++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java
@@ -26,7 +26,7 @@ void xsltTransformsXml() throws IOException {
.statusCode(200)
.contentType(JSON)
.body("books.size()", greaterThan(0))
- .body("books[0].year", equalTo(1975));
+ .body("books[0].year", equalTo("1975"));
// @formatter:on
}
}