* It will print all bytes transmitted or received by the HttpClient on a HTTP or HTTPS connection. *
- * HTTPS content will be printed unencrypted. + * HTTPS content will be printed unencryptedly. *
* This will create log entries like the following:
*
- * 08:15:50,047 TRACE 56 router /127.0.0.1:50835 ByteStreamLogging:52 {} - [c-569960249 out] [ 71 69 84 32 47 32 ... ] GET / HTTP/1.1
+ * 08:15:50,047 TRACE 56 router /127.0.0.1:50835 ByteStreamLogging:52 {} - [c-569960249 out] [ 71 69 84 32 47 32 ... ]
+ * GET / HTTP/1.1
* User-Agent: Jakarta Commons-HttpClient/3.1
* ...
*
@@ -59,7 +61,7 @@ public static void log(String name, int b){
}
public static void log(String name, byte... b){
- log(name, b,0,b.length);
+ log(name, b, 0, b.length);
}
public static void log(String name, byte[] b, int off, int len){
@@ -68,8 +70,9 @@ public static void log(String name, byte[] b, int off, int len){
for(int i = off; i < off+len; i++) {
sb.append(b[i]).append(" ");
}
- sb.append("] ");
- sb.append(new String(b,off,len, US_ASCII));
+ sb.append("]");
+ sb.append(lineSeparator());
+ sb.append(new String(b, off, len, US_ASCII));
log.trace(sb.toString());
}
@@ -89,7 +92,7 @@ public void write(byte[] b) throws IOException {
@Override
public void write(byte[] b, int off, int len) throws IOException {
- log(name, b,off,len);
+ log(name, b, off, len);
out.write(b, off, len);
}
@@ -110,21 +113,24 @@ public static InputStream wrapConnectionInputStream(InputStream in, String name)
@Override
public int read() throws IOException {
int res = in.read();
- log(name, res);
+ if (res != -1)
+ log(name, res);
return res;
}
@Override
public int read(byte[] b) throws IOException {
int res = in.read(b);
- log(name, b);
+ if (res != -1)
+ log(name, b, 0, res);
return res;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int res = in.read(b, off, len);
- log(name, b,off,len);
+ if (res != -1)
+ log(name, b, off, res);
return res;
}
diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java
index 8c3d9a4c87..bcc9b17ade 100644
--- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java
+++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java
@@ -85,9 +85,8 @@ private boolean dispatchCall(Exchange exc, String target, int attempt) throws Ex
protocolHandlerFactory.getHandlerForConnection(exc, outConType).handle(exc, outConType, hcp);
- if (trackNodeStatus(exc)) {
- exc.setNodeStatusCode(attempt, exc.getResponse().getStatusCode());
- }
+ if (exc.getNodeStatusTracker() != null)
+ exc.getNodeStatusTracker().setNodeStatusCode(attempt, exc.getResponse().getStatusCode());
// Check for protocol upgrades
String upgradedProtocol = exc.getProperty(UPGRADED_PROTOCOL, String.class);
diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java b/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java
index 50616284f5..705933defe 100644
--- a/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java
+++ b/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java
@@ -110,7 +110,8 @@ public void executeWithRetries(Exchange exc, RetryableCall call) throws Exceptio
}
log.debug("Retryable failure on attempt #{} to {}: {}", attempt, dest, e.getMessage());
- exc.trackNodeException(attempt, e);
+ if (exc.getNodeStatusTracker() != null)
+ exc.getNodeStatusTracker().trackNodeException(attempt, e);
}
if (attempt < retries) {
delayBetweenCalls(exc, currentDelay);
diff --git a/core/src/main/java/com/predic8/membrane/core/util/json/JsonToXml.java b/core/src/main/java/com/predic8/membrane/core/util/json/JsonToXml.java
new file mode 100644
index 0000000000..d11deefe12
--- /dev/null
+++ b/core/src/main/java/com/predic8/membrane/core/util/json/JsonToXml.java
@@ -0,0 +1,193 @@
+package com.predic8.membrane.core.util.json;
+
+import org.jetbrains.annotations.*;
+import org.json.*;
+
+import static java.lang.Boolean.*;
+import static org.json.JSONObject.*;
+
+public class JsonToXml {
+
+ // Prolog is needed to provide the UTF-8 encoding
+ static final String XML_PROLOG = "";
+
+ private String rootName = null;
+ private String arrayName = "array";
+ private String itemName = "item";
+
+ public JsonToXml rootName(String rootName) {
+ this.rootName = rootName;
+ return this;
+ }
+
+ public JsonToXml arrayName(String arrayName) {
+ this.arrayName = arrayName;
+ return this;
+ }
+
+ public JsonToXml itemName(String itemName) {
+ this.itemName = itemName;
+ return this;
+ }
+
+ public String toXml(String json) {
+ return XML_PROLOG + toXmlInternal(json);
+ }
+
+ String toXmlInternal(String json) {
+ Object input = parse(json);
+ StringBuilder sb = new StringBuilder();
+
+ // --- Case 1: Single-property object ---
+ if (rootName == null &&
+ input instanceof JSONObject jsonObj &&
+ jsonObj.keySet().size() == 1) {
+
+ String singleKey = jsonObj.keySet().iterator().next();
+ startTag(sb, singleKey);
+ build(jsonObj.get(singleKey), sb);
+ endTag(sb, singleKey);
+ return sb.toString();
+ }
+
+ // --- Case 2: Top-level array without explicit root ---
+ if (rootName == null && input instanceof JSONArray arr) {
+ startArray(sb);
+ buildArrayItems(sb, arr); // <- Important: NO nested array tag here
+ endArray(sb);
+ return sb.toString();
+ }
+
+ // --- Case 3: Normal case (object/primitive with root) ---
+ String effectiveRoot = rootName != null ? rootName : "root";
+
+ startTag(sb, effectiveRoot);
+ build(input, sb);
+ endTag(sb, effectiveRoot);
+ return sb.toString();
+ }
+
+ private static void endTag(StringBuilder sb, String singleKey) {
+ sb.append("").append(sanitizeXmlName( singleKey)).append(">");
+ }
+
+ private static void startTag(StringBuilder sb, String singleKey) {
+ sb.append("<").append(sanitizeXmlName( singleKey)).append(">");
+ }
+
+ static @NotNull Object parseLiteral(String t) {
+
+ // Try numeric types first (Double first to avoid Long overflow)
+ if (t.matches("-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?")) {
+ try {
+ double d = Double.parseDouble(t);
+
+ // If integer without decimal/exponent → try Long if it fits
+ if (t.matches("-?\\d+")) {
+ try {
+ return Long.parseLong(t);
+ } catch (NumberFormatException ignored) {
+ // too large → keep as Double
+ }
+ }
+
+ return d;
+
+ } catch (NumberFormatException e) {
+ // fallback: treat as string
+ return t;
+ }
+ }
+
+ // Check for quoted strings
+ if (t.startsWith("\"") && t.endsWith("\""))
+ return t.substring(1, t.length() - 1);
+
+ return t;
+ }
+
+
+ private Object parse(String jsonText) {
+ String t = jsonText.trim();
+
+ if (t.startsWith("{")) return new JSONObject(t);
+ if (t.startsWith("[")) return new JSONArray(t);
+ return switch (t) {
+ case "true" -> TRUE;
+ case "false" -> FALSE;
+ case "null" -> NULL;
+ default -> parseLiteral(t);
+
+ };
+ }
+
+ private void build(Object value, StringBuilder sb) {
+
+ if (value instanceof JSONObject jsonObj) {
+ buildObject(sb, jsonObj);
+ return;
+ }
+
+ if (value instanceof JSONArray array) {
+ buildArray(sb, array);
+ return;
+ }
+
+ if (value == null || value == NULL) {
+ return;
+ }
+ sb.append(escapeTextContent(String.valueOf(value)));
+ }
+
+ private void buildArray(StringBuilder sb, JSONArray array) {
+ startArray(sb);
+ buildArrayItems(sb, array);
+ endArray(sb);
+ }
+
+ private void buildArrayItems(StringBuilder sb, JSONArray array) {
+ for (int i = 0; i < array.length(); i++) {
+ startTag(sb, itemName);
+ build(array.get(i), sb);
+ endTag(sb, itemName);
+ }
+ }
+
+ private static String sanitizeXmlName(String key) {
+
+ // Replace any illegal XML characters
+ String sanitized = key.replaceAll("[^A-Za-z0-9_.-]", "_");
+
+ // XML element must NOT start with number or dot or hyphen
+ if (!sanitized.matches("[A-Za-z_].*")) {
+ sanitized = "_" + sanitized;
+ }
+
+ return sanitized;
+ }
+
+ private void buildObject(StringBuilder sb, JSONObject jsonObj) {
+ for (String key : jsonObj.keySet()) {
+ startTag(sb, key);
+ build(jsonObj.get(key), sb);
+ endTag(sb, key);
+ }
+ }
+
+ private void endArray(StringBuilder sb) {
+ endTag(sb, arrayName);
+ }
+
+ private void startArray(StringBuilder sb) {
+ startTag(sb, arrayName);
+ }
+
+ /**
+ * Not suitable for attribute values cause " and ' must be escaped there
+ */
+ private String escapeTextContent(String v) {
+ return v.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">");
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/predic8/membrane/core/util/JsonUtil.java b/core/src/main/java/com/predic8/membrane/core/util/json/JsonUtil.java
similarity index 98%
rename from core/src/main/java/com/predic8/membrane/core/util/JsonUtil.java
rename to core/src/main/java/com/predic8/membrane/core/util/json/JsonUtil.java
index 17f71b8f45..5fb73f8092 100644
--- a/core/src/main/java/com/predic8/membrane/core/util/JsonUtil.java
+++ b/core/src/main/java/com/predic8/membrane/core/util/json/JsonUtil.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.json;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
diff --git a/core/src/main/resources/com/predic8/membrane/core/interceptor/administration/docBase/admin/js/membrane.js b/core/src/main/resources/com/predic8/membrane/core/interceptor/administration/docBase/admin/js/membrane.js
index 28baf354c6..53f78c74ac 100644
--- a/core/src/main/resources/com/predic8/membrane/core/interceptor/administration/docBase/admin/js/membrane.js
+++ b/core/src/main/resources/com/predic8/membrane/core/interceptor/administration/docBase/admin/js/membrane.js
@@ -147,7 +147,11 @@ var membrane = function() {
});
} else {
loadText('#request-body', requestBodyUrl);
- }
+ }
+
+ if (exc.nodeStatusCodes) {
+ $('#node-status').attr('style', '');
+ }
}, 'json');
$('#request-meta').dataTable(getMetaTableConfiguration(function(data) {
@@ -161,6 +165,18 @@ var membrane = function() {
["Content Type", data.reqContentType],
["Length", data.reqContentLength]
];}));
+ $('#request-node-status').dataTable(getMetaTableConfiguration(function(data) {
+ var res = [];
+ for (var i = 0; i < data.destinations.length; i++) {
+ var status = "";
+ if (data.nodeExceptions && i < data.nodeExceptions.length && data.nodeExceptions[i])
+ status = data.nodeExceptions[i];
+ else if (data.nodeStatusCodes && i < data.nodeStatusCodes.length && data.nodeStatusCodes[i])
+ status = data.nodeStatusCodes[i];
+ res.push([data.destinations[i], status]);
+ }
+ return res;
+ }));
$('#request-headers').dataTable(getHeaderTableConfiguration(requestHeaderUrl));
loadText('#request-raw', requestRawUrl);
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInResponseFlowInternalRoutingTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInResponseFlowInternalRoutingTest.java
index a1043d2161..ca5a5a2a23 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInResponseFlowInternalRoutingTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInResponseFlowInternalRoutingTest.java
@@ -27,7 +27,7 @@ protected void configure() throws Exception {
api(api -> {
api.setKey(new ServiceProxyKey("*", "*", null, 2000));
api.add(A);
- api.getTarget().setUrl("internal:a");
+ api.getTarget().setUrl("internal://a");
});
internal(api -> {
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInternalRoutingTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInternalRoutingTest.java
index af3628cde9..6987077e06 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInternalRoutingTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/AbortInternalRoutingTest.java
@@ -29,7 +29,7 @@ protected void configure() throws Exception {
api(api -> {
api.setKey(new ServiceProxyKey("*", "*", null, 2000));
api.add(A);
- api.getTarget().setUrl("internal:a");
+ api.getTarget().setUrl("internal://a");
});
internal(api -> {
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/RequestResponseInternalRoutingTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/RequestResponseInternalRoutingTest.java
index d11330b9f0..740fa47919 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/RequestResponseInternalRoutingTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/RequestResponseInternalRoutingTest.java
@@ -29,7 +29,7 @@ protected void configure() throws Exception {
api(api -> {
api.setKey(new ServiceProxyKey("*","*",null,2000));
api.add(A);
- api.getTarget().setUrl("internal:a");
+ api.getTarget().setUrl("internal://a");
});
internal(api -> {
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/SimpleInternalRoutingTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/SimpleInternalRoutingTest.java
index d999eefef5..0fefe7ef36 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/SimpleInternalRoutingTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/internalservice/SimpleInternalRoutingTest.java
@@ -29,7 +29,7 @@ protected void configure() throws Exception {
api(api -> {
api.setKey(new ServiceProxyKey("*","*",null,2000));
api.add(A);
- api.getTarget().setUrl("internal:a");
+ api.getTarget().setUrl("internal://a");
});
internal(api -> {
diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptorTest.java
index dbe1354c5d..3d6620d5cb 100644
--- a/core/src/test/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptorTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/interceptor/xml/Json2XmlInterceptorTest.java
@@ -16,6 +16,8 @@
import com.predic8.membrane.core.*;
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.http.*;
+import io.restassured.path.json.*;
+import io.restassured.path.xml.*;
import org.junit.jupiter.api.*;
import org.xml.sax.*;
@@ -69,8 +71,8 @@ void normalRequest() throws Exception {
assertEquals(CONTINUE, interceptor.handleRequest(exc));
Message msg = exc.getRequest();
assertTrue(msg.isXML());
- assertEquals("Mike", xPath(msg.getBodyAsStringDecoded(), "/person/name"));
- assertEquals("San Francisco", xPath(msg.getBodyAsStringDecoded(), "/person/city"));
+ assertXPath("Mike", msg, "/person/name");
+ assertXPath("San Francisco", msg, "/person/city");
assertTrue(msg.getBodyAsStringDecoded().contains(UTF_8.name()));
}
@@ -81,8 +83,8 @@ void normalResponse() throws Exception {
assertEquals(CONTINUE, interceptor.handleResponse(exc));
Message msg = exc.getResponse();
assertTrue(msg.isXML());
- assertEquals("Mike", xPath(msg.getBodyAsStringDecoded(), "/person/name"));
- assertEquals("San Francisco", xPath(msg.getBodyAsStringDecoded(), "/person/city"));
+ assertXPath("Mike", msg, "/person/name");
+ assertXPath("San Francisco", msg, "/person/city");
}
@Test
@@ -91,7 +93,35 @@ void single() throws Exception {
assertEquals(CONTINUE, interceptor.handleRequest(exc));
Message msg = exc.getRequest();
assertTrue(msg.isXML());
- assertEquals("Berlin", xPath(msg.getBodyAsStringDecoded(), "/place"));
+ assertXPath("Berlin", msg, "/place");
+ }
+
+ @Test
+ void nested() throws URISyntaxException {
+ Exchange exc = put("/nested").json("""
+ {
+ "one": [1],
+ "nested": [[2]],
+ "three": [[[3]]]
+ }
+ """).buildExchange();
+
+ assertEquals(CONTINUE, interceptor.handleRequest(exc));
+
+ String xml = exc.getRequest().getBodyAsStringDecoded();
+ XmlPath xp = new XmlPath(xml);
+
+ // one = [1]
+ assertEquals(1, xp.getInt("root.one.array.item"));
+ assertEquals(1, xp.getList("root.one.array.item").size());
+
+ // nested = [[2]]
+ assertEquals(2, xp.getInt("root.nested.array.item.array.item"));
+ assertEquals(1, xp.getList("root.nested.array.item.array.item").size());
+
+ // three = [[[3]]]
+ assertEquals(3, xp.getInt("root.three.array.item.array.item.array.item"));
+ assertEquals(1, xp.getList("root.three.array.item.array.item.array.item").size());
}
@Test
@@ -100,31 +130,73 @@ void noRoot() throws Exception {
assertEquals(CONTINUE, interceptor.handleRequest(exc));
Message msg = exc.getRequest();
assertTrue(msg.isXML());
- assertEquals("1", xPath(msg.getBodyAsStringDecoded(), "/root/a"));
- assertEquals("2", xPath(msg.getBodyAsStringDecoded(), "/root/b"));
+ assertXPath("1", msg, "/root/a");
+ assertXPath("2", msg, "/root/b");
}
@Test
void noRootWithRootNameSpecified() throws Exception {
interceptor.setRoot("top");
+ interceptor.init();
Exchange exc = put("/no-root").json(noRoot).buildExchange();
assertEquals(CONTINUE, interceptor.handleRequest(exc));
Message msg = exc.getRequest();
assertTrue(msg.isXML());
- assertEquals("1", xPath(msg.getBodyAsStringDecoded(), "/top/a"));
- assertEquals("2", xPath(msg.getBodyAsStringDecoded(), "/top/b"));
+ assertXPath("1", msg, "/top/a");
+ assertXPath("2", msg, "/top/b");
+ }
+
+ @Test
+ void array() throws URISyntaxException, XPathExpressionException {
+ Exchange exc = put("/array").json("[1,2,3]").buildExchange();
+ assertEquals(CONTINUE, interceptor.handleRequest(exc));
+ Message msg = exc.getRequest();
+ assertXPath("1",msg,"/array/item[1]");
+ assertXPath("2",msg,"/array/item[2]");
+ assertXPath("3",msg,"/array/item[3]");
}
@Test
- void invalidJSON() throws URISyntaxException {
- Exchange exc = put("/invalid").json("{ invalid").buildExchange();
+ void arrayWithRoot() throws URISyntaxException, XPathExpressionException {
+ interceptor.setRoot("myRoot");
+ interceptor.init();
+ Exchange exc = put("/array").json("[1,2,3]").buildExchange();
+ assertEquals(CONTINUE, interceptor.handleRequest(exc));
+ var msg = exc.getRequest();
+ assertXPath("1",msg,"/myRoot/array/item[1]");
+ assertXPath("2",msg,"/myRoot/array/item[2]");
+ assertXPath("3",msg,"/myRoot/array/item[3]");
+ }
+
+ @Test
+ void arrayOneElement() throws Exception {
+ Exchange exc = put("/array").json("[1]").buildExchange();
+ assertEquals(CONTINUE, interceptor.handleRequest(exc));
+ assertXPath("1", (Message) exc.getRequest(),"/array/item[1]");
+ }
+
+ @Test
+ void number() throws Exception {
+ Exchange exc = put("/number").json("1").buildExchange();
+ assertEquals(CONTINUE, interceptor.handleRequest(exc));
+ Message msg = exc.getRequest();
+ assertXPath("1", msg, "/root");
+ }
+
+ @Test
+ void invalid() throws URISyntaxException {
+ Exchange exc = put("/invalid").json("{").buildExchange();
assertEquals(ABORT, interceptor.handleRequest(exc));
- assertTrue(exc.getResponse().getBodyAsStringDecoded().contains("Error parsing JSON"));
+ Response res = exc.getResponse();
+ assertEquals(500, res.getStatusCode());
+ assertEquals("Error parsing JSON", new JsonPath(res.getBodyAsStringDecoded()).get("title"));
}
private static String xPath(String body, String expression) throws XPathExpressionException {
- System.out.println("body = " + body);
return xPathFactory.newXPath().evaluate(expression, new InputSource(new StringReader(body)));
}
+ private void assertXPath(String expected, Message msg, String xpath) throws XPathExpressionException {
+ assertEquals(expected, xPath(msg.getBodyAsStringDecoded(), xpath));
+ }
}
diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/ConnectionKeepAliveTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/ConnectionKeepAliveTest.java
new file mode 100644
index 0000000000..2fae536cdb
--- /dev/null
+++ b/core/src/test/java/com/predic8/membrane/core/transport/http/ConnectionKeepAliveTest.java
@@ -0,0 +1,104 @@
+package com.predic8.membrane.core.transport.http;
+
+import com.predic8.membrane.core.HttpRouter;
+import com.predic8.membrane.core.Router;
+import com.predic8.membrane.core.config.Path;
+import com.predic8.membrane.core.exchange.Exchange;
+import com.predic8.membrane.core.http.Request;
+import com.predic8.membrane.core.interceptor.flow.ResponseInterceptor;
+import com.predic8.membrane.core.interceptor.flow.ReturnInterceptor;
+import com.predic8.membrane.core.interceptor.groovy.GroovyInterceptor;
+import com.predic8.membrane.core.interceptor.templating.StaticInterceptor;
+import com.predic8.membrane.core.openapi.serviceproxy.APIProxy;
+import com.predic8.membrane.core.openapi.serviceproxy.OpenAPISpec;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static com.predic8.membrane.core.openapi.serviceproxy.OpenAPISpec.YesNoOpenAPIOption.YES;
+
+public class ConnectionKeepAliveTest {
+
+ Router gateway;
+ Router backend;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ gateway = getGateway();
+ backend = getBackend();
+ }
+
+ @Test
+ void foo() throws Exception{
+ HttpClient client = new HttpClient();
+ Exchange exc = Request.get("http://localhost:2000/health").buildExchange();
+ client.call(exc);
+ client.call(exc);
+ }
+
+ private @NotNull Router getGateway() throws Exception{
+ HttpRouter router = new HttpRouter();
+ router.getRuleManager().addProxyAndOpenPortIfNew(getOpenApiProxy());
+ router.setBaseLocation("src/test/resources/");
+ router.init();
+ return router;
+ }
+
+ private static @NotNull Router getBackend() throws Exception {
+ HttpRouter router = new HttpRouter();
+ router.getRuleManager().addProxyAndOpenPortIfNew(getHealthProxy());
+ router.getRuleManager().addProxyAndOpenPortIfNew(getFooProxy());
+ router.init();
+ return router;
+ }
+
+ private @NotNull APIProxy getOpenApiProxy() {
+ APIProxy openApiProxy = new APIProxy();
+ openApiProxy.setPort(2000);
+ openApiProxy.setSpecs(List.of(new OpenAPISpec() {{
+ setLocation("health.yaml");
+ setValidateRequests(YES);
+ setValidationDetails(YES);
+ setValidateResponses(YES);
+ setValidateSecurity(YES);
+ }}));
+ return openApiProxy;
+ }
+
+ private static @NotNull APIProxy getFooProxy() {
+ APIProxy fooProxy = new APIProxy();
+ fooProxy.setPort(3000);
+ fooProxy.setPath(new Path(false, "/foo"));
+ fooProxy.setFlow(List.of(
+ new ResponseInterceptor() {{
+ setFlow(List.of(
+ new StaticInterceptor() {{
+ setContentType("application/json");
+ setSrc("{\"status\": \"ok\"}");
+ }})
+ );
+ }}, new ReturnInterceptor()
+ ));
+ return fooProxy;
+ }
+
+ private static @NotNull APIProxy getHealthProxy() {
+ APIProxy healthProxy = new APIProxy();
+ healthProxy.setPort(3000);
+ healthProxy.setPath(new Path(false, "/health"));
+ healthProxy.setFlow(List.of(new GroovyInterceptor() {{
+ setSrc("""
+ exchange.setResponse(Response.ok("No Matching Rule matched.").build())
+ exchange.getResponse().getHeader().add("Transfer-Encoding", "chunked")
+ exchange.getResponse().getHeader().removeFields("Content-length")
+ exchange.getResponse().setStatusCode(400)
+ sleep(100)
+ return RETURN
+ """);
+ }}));
+ return healthProxy;
+ }
+
+}
diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/ServiceInvocationTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/ServiceInvocationTest.java
index f2b5c0326d..513af03008 100644
--- a/core/src/test/java/com/predic8/membrane/core/transport/http/ServiceInvocationTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/transport/http/ServiceInvocationTest.java
@@ -56,7 +56,7 @@ public void tearDown() {
private ServiceProxy createFirstRule() {
ServiceProxy rule = new ServiceProxy(new ServiceProxyKey("localhost", METHOD_POST, "*", 2000), "localhost", 80);
- rule.getTarget().setUrl("internal:log");
+ rule.getTarget().setUrl("internal://log");
rule.getFlow().add(new MockInterceptor("process"));
return rule;
}
diff --git a/core/src/test/java/com/predic8/membrane/core/util/JsonUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/JsonUtilTest.java
index 30feb97005..921be3b3e8 100644
--- a/core/src/test/java/com/predic8/membrane/core/util/JsonUtilTest.java
+++ b/core/src/test/java/com/predic8/membrane/core/util/JsonUtilTest.java
@@ -17,7 +17,7 @@
import com.fasterxml.jackson.databind.*;
import org.junit.jupiter.api.*;
-import static com.predic8.membrane.core.util.JsonUtil.scalarAsJson;
+import static com.predic8.membrane.core.util.json.JsonUtil.scalarAsJson;
import static org.junit.jupiter.api.Assertions.*;
class JsonUtilTest {
diff --git a/core/src/test/java/com/predic8/membrane/core/util/json/JsonToXmlTest.java b/core/src/test/java/com/predic8/membrane/core/util/json/JsonToXmlTest.java
new file mode 100644
index 0000000000..82a426d408
--- /dev/null
+++ b/core/src/test/java/com/predic8/membrane/core/util/json/JsonToXmlTest.java
@@ -0,0 +1,228 @@
+package com.predic8.membrane.core.util.json;
+
+import io.restassured.path.xml.*;
+import org.junit.jupiter.api.*;
+
+import static com.predic8.membrane.core.util.json.JsonToXml.XML_PROLOG;
+import static com.predic8.membrane.core.util.json.JsonToXml.parseLiteral;
+import static org.junit.jupiter.api.Assertions.*;
+
+class JsonToXmlTest {
+
+ XmlPath xp;
+ JsonToXml conv;
+
+ @BeforeEach
+ void setup() {
+ conv = new JsonToXml().rootName("root").arrayName("list").itemName("item");
+ }
+
+ @Nested
+ class Configuration {
+
+ @Test
+ void noRootNameSet() {
+ conv.rootName(null);
+ XmlPath xp = new XmlPath(conv.toXml("{}"));
+ assertEquals("", xp.get("root"));
+ }
+
+ @Test
+ void topLevelArray_withoutRoot_usesArrayNameAsRoot() {
+ conv.rootName(null).arrayName("items");
+ assertEquals(2, new XmlPath(conv.toXml("[1,2]")).getList("items.item").size());
+ }
+ }
+
+
+ @Nested
+ class ObjectsAndArrays {
+
+ @BeforeEach
+ void setup() {
+ var json = """
+ {
+ "one": [1],
+ "nested": [[2]],
+ "three": [[[3]]],
+ "different": [
+ {"foo":4},
+ [1,2,[3,[4]]],
+ 5
+ ]
+ }
+ """;
+ xp = new XmlPath(conv.toXml(json));
+ }
+
+ @Test
+ void one_isPreserved() {
+ assertEquals(1, xp.getInt("root.one.list.item"));
+ assertEquals(1, xp.getList("root.one.list.item").size());
+ }
+
+ @Test
+ void nested_isPreserved() {
+ assertEquals(1, xp.getList("root.nested.list.item.list.item").size());
+ assertEquals(2, xp.getInt("root.nested.list.item.list.item"));
+ }
+
+ @Test
+ void three_isPreserved() {
+ assertEquals(3, xp.getInt("root.three.list.item.list.item.list.item"));
+ assertEquals(1, xp.getList("root.three.list.item.list.item.list.item").size());
+ }
+
+ @Test
+ void different_isPreserved() {
+ // {"foo":4}
+ assertEquals(4, xp.getInt("root.different.list.item[0].foo"));
+
+ // [1,2,[3,[4]]]
+ assertEquals(2, xp.getInt("root.different.list.item[1].list.item[1]"));
+ assertEquals(3, xp.getInt("root.different.list.item[1].list.item[2].list.item[0]"));
+ assertEquals(4, xp.getInt("root.different.list.item[1].list.item[2].list.item[1].list.item"));
+
+ // 5
+ assertEquals(5, xp.getInt("root.different.list.item[2]"));
+ }
+ }
+
+ @Nested
+ class Objects {
+
+ @Test
+ void singleProperty() {
+ conv.rootName(null);
+
+ String xml = conv.toXml("""
+ {
+ "person": {
+ "name": "John",
+ "age": 30
+ }
+ }
+ """);
+ XmlPath xp = new XmlPath(xml);
+ assertEquals("John", xp.getString("person.name"));
+ assertEquals("30", xp.getString("person.age"));
+ }
+
+ }
+
+ @Nested
+ class Arrays {
+
+ @Test
+ void emptyArray_isConverted() {
+ String xml = conv.toXml("[]");
+ XmlPath xp = new XmlPath(xml);
+
+ assertNotNull(xp.get("root.list"));
+ assertEquals("", xp.get("root.list"));
+ assertTrue(xml.contains("