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 8eb193cd95..d45250c32b 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,111 +19,127 @@ 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.StringUtil.*; +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, exc.getStringProperties()); + } catch (TransformerException e) { + log.debug("", e); + 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", truncateAfter(msg.getBodyAsStringDecoded() + "...", 50)) + .buildAndSetResponse(exc); + return ABORT; + } + return createErrorResponse(exc,e,flow); } 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,flow); } 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, Flow flow) { + user(router.getConfiguration().isProduction(), getDisplayName()) + .detail("Error transforming message!") + .exception(e) + .internal("flow", flow.toString()) + .buildAndSetResponse(exc); + return ABORT; + } + + private void transformMsg(Message msg, 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) { - log.debug("",e); + log.debug("",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 d8384ad590..e2e060ce56 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..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 @@ -13,79 +13,84 @@ 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 noContentInProlog() 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(); + assertTrue(body.contains("rubbish")); + assertTrue(body.contains("not allowed in prolog")); + } + + 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/XsltTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltTransformationTutorialTest.java index e7d36b7022..300fdc5285 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 @@ -25,7 +25,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/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.java new file mode 100644 index 0000000000..9bfcc3fbba --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/xml/XsltXML2JSONTransformationTutorialTest.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 XsltXML2JSONTransformationTutorialTest 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..54e47b4e03 --- /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 Content-Type to format the output as JSON + - 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..156fe22242 --- /dev/null +++ b/distribution/tutorials/xml/to-json.xsl @@ -0,0 +1,30 @@ + + + + + + + + { "books": [ + + ] } + + + + + { + + } + + , + + + + + "": "" + , + + + + \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 468b838e86..16300a9621 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