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 } }