From 477d87df36de70c6009f1615fd6eba8aa67bf97f Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 8 Jan 2026 13:54:54 +0100 Subject: [PATCH 01/15] refactor(wsdl): streamline WSDL handling, improve logging, and adjust interceptor flow - Removed unused fields `blockRequest` and `blockResponse` in `AbstractProxy`. - Simplified `log.debug` messages in `WSDLPublisherInterceptor`. - Adjusted interceptor execution order in `SOAPProxy` by adding `WSDLPublisherInterceptor` to the end. - Changed `@MCElement` for `WSDLInterceptor` to include it in the flow. - Improved readability with `formatted` and repositioned imports for `notFound`. --- .../membrane/core/interceptor/WSDLInterceptor.java | 2 +- .../interceptor/server/WSDLPublisherInterceptor.java | 11 ++++++----- .../predic8/membrane/core/proxies/AbstractProxy.java | 3 --- .../com/predic8/membrane/core/proxies/SOAPProxy.java | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 67ecb950c7..6798ce6706 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -36,7 +36,7 @@ * @description

The wsdlRewriter rewrites endpoint addresses of services and XML Schema locations in WSDL documents.

* @topic 5. Web Services with SOAP and WSDL */ -@MCElement(name = "wsdlRewriter", excludeFromFlow = true) +@MCElement(name = "wsdlRewriter", excludeFromFlow = false) public class WSDLInterceptor extends RelocatingInterceptor { private final static Logger log = LoggerFactory.getLogger(WSDLInterceptor.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java index edc234251d..294a3d8d35 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java @@ -29,6 +29,7 @@ import static com.predic8.membrane.core.exceptions.ProblemDetails.*; import static com.predic8.membrane.core.http.MimeType.*; import static com.predic8.membrane.core.http.Response.*; +import static com.predic8.membrane.core.http.Response.notFound; import static com.predic8.membrane.core.interceptor.Outcome.*; import static com.predic8.membrane.core.resolver.ResolverMap.combine; @@ -54,9 +55,9 @@ public WSDLPublisherInterceptor() { /** * Note that this class fulfills two purposes: *

- * * During the initial processDocuments() run, the XSDs are enumerated. + * During the initial processDocuments() run, the XSDs are enumerated. *

- * * During later runs (as well as the initial run, but that's result is discarded), + * During later runs (as well as the initial run, but that result is discarded), * the documents are rewritten. */ private final class RelativePathRewriter implements PathRewriter { @@ -109,7 +110,7 @@ private void processDocuments(Exchange exc) { String doc = documents_to_process.poll(); if (doc == null) break; - log.debug("WSDLPublisherInterceptor: processing {}", doc); + log.debug("processing: {}", doc); exc.setResponse(webServerInterceptor.createResponse(router.getResolverMap(), doc)); WSDLInterceptor wi = new WSDLInterceptor(); wi.setRewriteEndpoint(false); @@ -218,7 +219,7 @@ private Outcome handleRequestInternal(final Exchange exc) throws Exception { exc.setResponse(HttpUtil.setHTMLErrorResponse(Response.internalServerError(), "Bad parameter format.", "")); return ABORT; } catch (ResourceRetrievalException e) { - exc.setResponse(Response.notFound().build()); + exc.setResponse(notFound().build()); return ABORT; } @@ -227,7 +228,7 @@ private Outcome handleRequestInternal(final Exchange exc) throws Exception { @Override public String getShortDescription() { - return "Publishes the WSDL at " + wsdl + " under \"?wsdl\" (as well as its dependent schemas under similar URLs)."; + return "Publishes the WSDL at %s under \"?wsdl\" (as well as its dependent schemas under similar URLs).".formatted(wsdl); } public void setSoapProxy(SOAPProxy soapProxy) { diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractProxy.java index 3d5fb95085..798a7354e0 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/AbstractProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/AbstractProxy.java @@ -33,9 +33,6 @@ public abstract class AbstractProxy implements Proxy { protected String name = ""; protected RuleKey key; - protected volatile boolean blockRequest; - protected volatile boolean blockResponse; - protected List interceptors = new ArrayList<>(); private final RuleStatisticCollector ruleStatisticCollector = new RuleStatisticCollector(); diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java index ab35ba5fe2..62a80a69f7 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java @@ -329,7 +329,7 @@ private void addWSDLPublisherInterceptor() { WSDLPublisherInterceptor wp = new WSDLPublisherInterceptor(); wp.setWsdl(wsdl); wp.init(router); - interceptors.addFirst(wp); + interceptors.addLast(wp); } private boolean hasWSDLPublisherInterceptor() { From a75983f2be208589fc013fcb1d0e250dd99b8962 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 8 Jan 2026 14:23:24 +0100 Subject: [PATCH 02/15] add: tutorial for SOAP proxy with YAML configuration and integration test - Introduced `20-SOAPProxy.yaml` for configuring a SOAP proxy example. - Added `SOAPProxyTutorialTest` and `AbstractSOAPTutorialTest` for integration testing. --- .../soap/AbstractSOAPTutorialTest.java | 26 +++++++++++++++++ .../tutorials/soap/SOAPProxyTutorialTest.java | 28 +++++++++++++++++++ distribution/tutorials/soap/20-SOAPProxy.yaml | 8 ++++++ 3 files changed, 62 insertions(+) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractSOAPTutorialTest.java create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java create mode 100644 distribution/tutorials/soap/20-SOAPProxy.yaml diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractSOAPTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractSOAPTutorialTest.java new file mode 100644 index 0000000000..94696a17f4 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractSOAPTutorialTest.java @@ -0,0 +1,26 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +import com.predic8.membrane.tutorials.*; + +public abstract class AbstractSOAPTutorialTest extends AbstractMembraneTutorialTest { + + @Override + protected String getTutorialDir() { + return "soap"; + } + +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java new file mode 100644 index 0000000000..f70d6f7e78 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java @@ -0,0 +1,28 @@ +package com.predic8.membrane.tutorials.soap; + +import org.junit.jupiter.api.*; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; + +public class SOAPProxyTutorialTest extends AbstractSOAPTutorialTest { + + @Override + protected String getTutorialYaml() { + return "20-SOAPProxy.yaml"; + } + + @Test + void adminConsole() { + // @formatter:off + given() + .when() + .get("http://localhost:2000/city-service?wsdl") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(containsString("definitions")); + // @formatter:on + } + +} diff --git a/distribution/tutorials/soap/20-SOAPProxy.yaml b/distribution/tutorials/soap/20-SOAPProxy.yaml new file mode 100644 index 0000000000..941e268bad --- /dev/null +++ b/distribution/tutorials/soap/20-SOAPProxy.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# Membrane Tutorial: SOAP Proxy +# + +soapProxy: + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl \ No newline at end of file From 7eb7420a1aa07d54b77f01c9c2cb910f5896fa3b Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 8 Jan 2026 21:15:54 +0100 Subject: [PATCH 03/15] add: WSDL rewriting example, SOAP tutorial tests, and script refactor - Introduced `30-WSDL-Rewriter.yaml` for demonstrating WSDL rewriting functionality. - Added `WSDLRewriterTutorialTest` and expanded `AbstractCityServiceTest` for reusable SOAP test logic. - Rewrote `membrane.sh` and added `membrane.cmd` for improved script execution and structure detection. - Removed `invoke-sample-soap-service.sh` and integrated SOAP request example directly into YAML tutorials. - Enhanced SOAP proxy examples in `20-SOAPProxy.yaml` and `10-Sample-SOAP-Service.yaml`. - Updated `README.md` with new tutorial topics and streamlined documentation. --- .../predic8/membrane/core/cli/RouterCLI.java | 5 +- .../core/interceptor/WSDLInterceptor.java | 6 +- .../soap/AbstractCityServiceTest.java | 44 ++++++++ .../tutorials/soap/SOAPProxyTutorialTest.java | 32 ++++-- .../soap/SampleSOAPServiceTutorialTest.java | 9 ++ .../soap/WSDLRewriterTutorialTest.java | 75 +++++++++++++ .../tutorials/data/city-request.soap.xml | 7 ++ .../soap/10-Sample-SOAP-Service.yaml | 20 +--- distribution/tutorials/soap/20-SOAPProxy.yaml | 17 ++- .../tutorials/soap/30-WSDL-Rewriter.yaml | 29 +++++ distribution/tutorials/soap/README.md | 3 +- .../soap/invoke-sample-soap-service.sh | 15 --- distribution/tutorials/soap/membrane.cmd | 24 ++++ distribution/tutorials/soap/membrane.sh | 105 ++++-------------- 14 files changed, 262 insertions(+), 129 deletions(-) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java create mode 100644 distribution/tutorials/data/city-request.soap.xml create mode 100644 distribution/tutorials/soap/30-WSDL-Rewriter.yaml delete mode 100755 distribution/tutorials/soap/invoke-sample-soap-service.sh create mode 100644 distribution/tutorials/soap/membrane.cmd diff --git a/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java b/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java index b8fa3e0a39..ca6dc59884 100644 --- a/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java +++ b/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java @@ -56,6 +56,7 @@ public static void main(String[] args) { } private static void start(String[] args) { + log.debug("CLI started with args: {}", Arrays.toString(args)); MembraneCommandLine commandLine = getMembraneCommandLine(args); if (commandLine.getCommand().isOptionSet("h")) { commandLine.getCommand().printHelp(); @@ -249,7 +250,7 @@ private static String getRulesFile(MembraneCommandLine cl) throws IOException { try (InputStream ignored = rm.resolve(filename)) { return filename; } catch (ResourceRetrievalException e) { - System.err.println("Could not open Membrane's configuration file: " + filename + " not found."); + System.err.printf("Could not open Membrane's configuration file: %s not found.%n", filename); System.exit(1); } } @@ -339,7 +340,7 @@ private static boolean canResolveConfigurationFile(ResolverMap rm, String try1, try (InputStream ignored = rm.resolve(try1)) { return true; } catch (Exception e) { - log.warn("Could not resolve path to configuration (attempt {}).", attempt, e); + log.warn("Could not resolve path to configuration (path: {} attempt: {}).", try1, attempt); } return false; } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 6798ce6706..310f86818e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -200,8 +200,8 @@ public void setHost(String host) { * @example 4000 */ @MCAttribute - @Override - public void setPort(String port) { - super.setPort(port); + public void setPort(int port) { + super.setPort(Integer.toString(port)); } + } diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java new file mode 100644 index 0000000000..b9075b29f5 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java @@ -0,0 +1,44 @@ +package com.predic8.membrane.tutorials.soap; + +import org.junit.jupiter.api.*; + +import java.io.*; + +import static com.predic8.membrane.core.Constants.WSDL_SOAP11_NS; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.XML; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public abstract class AbstractCityServiceTest extends AbstractSOAPTutorialTest { + + @Test + void wsdl() { + // @formatter:off + given() + .when() + .get("http://localhost:2000/city-service?wsdl") + .then() + .log().body() + .statusCode(200) + .contentType(XML) + .body(containsString(WSDL_SOAP11_NS)) + + // s:address/@location must be rewritten + .body("definitions.service.port.address.@location", equalTo("http://localhost:2000/city-service")); + // @formatter:on + } + + @Test + void soapCall() throws IOException { + given() + // File is read from FS use the same file as the user + .body(readFileFromBaseDir("../data/city-request.soap.xml")) + .when() + .post("http://localhost:2000/city-service") + .then() + .log().body() + .body("Envelope.Body.getCityResponse.population", equalTo("34665600")); + } + +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java index f70d6f7e78..91336055ad 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java @@ -1,11 +1,26 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.tutorials.soap; import org.junit.jupiter.api.*; -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.containsString; +import static io.restassured.RestAssured.*; +import static io.restassured.http.ContentType.*; +import static org.hamcrest.Matchers.*; -public class SOAPProxyTutorialTest extends AbstractSOAPTutorialTest { +public class SOAPProxyTutorialTest extends AbstractCityServiceTest { @Override protected String getTutorialYaml() { @@ -13,16 +28,17 @@ protected String getTutorialYaml() { } @Test - void adminConsole() { + void webServiceExplorer() { // @formatter:off given() + .body("Invalid") .when() - .get("http://localhost:2000/city-service?wsdl") + .get("http://localhost:2000/city-service") .then() - .log().ifValidationFails() + .log().body() .statusCode(200) - .body(containsString("definitions")); + .contentType(HTML) + .body(containsString("Membrane API Gateway: CityService")); // @formatter:on } - } diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java new file mode 100644 index 0000000000..c9ad97644d --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java @@ -0,0 +1,9 @@ +package com.predic8.membrane.tutorials.soap; + +public class SampleSOAPServiceTutorialTest extends AbstractCityServiceTest { + + @Override + protected String getTutorialYaml() { + return "10-Sample-SOAP-Service.yaml"; + } +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java new file mode 100644 index 0000000000..84beec5816 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java @@ -0,0 +1,75 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +import org.junit.jupiter.api.*; + +import java.io.*; + +import static com.predic8.membrane.core.Constants.*; +import static io.restassured.RestAssured.*; +import static io.restassured.http.ContentType.*; +import static org.hamcrest.Matchers.*; + +public class WSDLRewriterTutorialTest extends AbstractSOAPTutorialTest { + + @Override + protected String getTutorialYaml() { + return "30-WSDL-Rewriter.yaml"; + } + + @Test + void wsdl() { + // @formatter:off + given() + .when() + .get("http://localhost:2000/my-service?wsdl") + .then() + .statusCode(200) + .contentType(XML) + .body(containsString(WSDL_SOAP11_NS)) + + // s:address/@location must be rewritten + .body("definitions.service.port.address.@location", equalTo("https://my.host.example.com/my-service")); + // @formatter:on + } + + @Test + void soapCall() throws IOException { + // @formatter:off + given() + // File is read from FS use the same file as the user + .body(readFileFromBaseDir("../data/city-request.soap.xml")) + .when() + .post("http://localhost:2000/my-service") + .then() + .body("Envelope.Body.getCityResponse.population", equalTo("34665600")); + // @formatter:on + } + + @Test + void webServiceExplorer() { + // @formatter:off + given() + .body("Invalid") + .when() + .get("http://localhost:2000/my-service") + .then() + .statusCode(200) + .contentType(HTML) + .body(containsString("Membrane API Gateway: CityService")); + // @formatter:on + } +} diff --git a/distribution/tutorials/data/city-request.soap.xml b/distribution/tutorials/data/city-request.soap.xml new file mode 100644 index 0000000000..9b30e1394e --- /dev/null +++ b/distribution/tutorials/data/city-request.soap.xml @@ -0,0 +1,7 @@ + + + + Delhi + + + \ No newline at end of file diff --git a/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml b/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml index 08587e2c70..0fccfb5309 100644 --- a/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml +++ b/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml @@ -1,28 +1,20 @@ # yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json # -# Membrane Tutorial: Sample SOAP Service +# Tutorial: Sample SOAP Service # -# The `sampleSoapService` is useful for development and debugging. -# -# 1.) Start Membrane: -# Open a terminal in this folder and run: +# A minimal SOAP endpoint for development and debugging. # +# 1.) Run # Linux/Mac: # ./membrane.sh -c 10-Sample-SOAP-Service.yaml # Windows: # membrane.cmd -c 10-Sample-SOAP-Service.yaml # # 2.) WSDL -# Open the WSDL in a browser: -# -# http://localhost:2000/city-service?wsdl -# -# 3.) Invoke the Web Service -# Run: -# -# ./invoke-sample-soap-service.sh +# http://localhost:2000/city-service?wsdl # -# Or import the WSDL into SoapUI +# 3.) Invoke +# curl -d @../data/city-request.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service api: port: 2000 diff --git a/distribution/tutorials/soap/20-SOAPProxy.yaml b/distribution/tutorials/soap/20-SOAPProxy.yaml index 941e268bad..3537adec66 100644 --- a/distribution/tutorials/soap/20-SOAPProxy.yaml +++ b/distribution/tutorials/soap/20-SOAPProxy.yaml @@ -1,7 +1,22 @@ # yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json # -# Membrane Tutorial: SOAP Proxy +# Tutorial: SOAP Proxy # +# A SOAP proxy configured from a WSDL document. +# +# When a WSDL is exposed through an API Gateway, it must be rewritten so that: +# - The service address points to the gateway, not the backend. +# - References to imported XSD schema files are adjusted accordingly. +# The serviceProxy rewrites the WSDL automatically on the fly. +# +# 1.) Start Membrane +# +# 2.) WSDL +# http://localhost:2000/city-service?wsdl +# Take a look at the address/@location attribute at the end of the WSDL +# +# 3.) Invoke +# curl -d @../data/city-request.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service soapProxy: port: 2000 diff --git a/distribution/tutorials/soap/30-WSDL-Rewriter.yaml b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml new file mode 100644 index 0000000000..5cf29166eb --- /dev/null +++ b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# Tutorial: WSDL Rewriter +# +# By default, WSDL rewriting uses the hostname and port on which the API Gateway +# is reachable. This is sufficient for direct access. +# +# If the API Gateway is deployed behind a reverse proxy or load balancer, these +# defaults are usually incorrect. In such cases, the public-facing host, protocol, +# port, and path must be rewritten explicitly. +# +# This example shows how to override these values. +# +# 1.) Start Membrane +# +# 2.) WSDL +# http://localhost:2000/my-service?wsdl +# Inspect the address/@location attribute near the end of the document. + +soapProxy: + path: + uri: /my-service + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl + flow: + - wsdlRewriter: + host: my.host.example.com + protocol: https + port: 443 diff --git a/distribution/tutorials/soap/README.md b/distribution/tutorials/soap/README.md index 6347701297..5d9fa2ba31 100644 --- a/distribution/tutorials/soap/README.md +++ b/distribution/tutorials/soap/README.md @@ -8,8 +8,9 @@ Start by looking at [10-Sample-SOAP-Service.yaml](10-Sample-SOAP-Service.yaml). Planned topics include: -- SOAPProxy - soapTemplate +- WSDL validation - More SOAP-related features +- REST to SOAP transformation More examples are available in the examples folder. \ No newline at end of file diff --git a/distribution/tutorials/soap/invoke-sample-soap-service.sh b/distribution/tutorials/soap/invoke-sample-soap-service.sh deleted file mode 100755 index 146113afad..0000000000 --- a/distribution/tutorials/soap/invoke-sample-soap-service.sh +++ /dev/null @@ -1,15 +0,0 @@ -curl -X POST http://localhost:2000/city-service \ - -H "Content-Type: text/xml" \ - -H "SOAPAction: https://predic8.de/cities" \ - -d @- <<'EOF' - - - - - Berlin - - - -EOF \ No newline at end of file diff --git a/distribution/tutorials/soap/membrane.cmd b/distribution/tutorials/soap/membrane.cmd new file mode 100644 index 0000000000..8d2d64e9cf --- /dev/null +++ b/distribution/tutorials/soap/membrane.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +set "dir=%SCRIPT_DIR%" + +:search_up +if exist "%dir%\LICENSE.txt" if exist "%dir%\scripts\run-membrane.cmd" goto found +for %%A in ("%dir%\..") do set "next=%%~fA" +if /I "%next%"=="%dir%" goto notfound +set "dir=%next%" +goto search_up + +:found +set "MEMBRANE_HOME=%dir%" +set "MEMBRANE_CALLER_DIR=%SCRIPT_DIR%" +call "%MEMBRANE_HOME%\scripts\run-membrane.cmd" %* +exit /b %ERRORLEVEL% + +:notfound +>&2 echo Could not locate Membrane root. Ensure directory structure is correct. +exit /b 1 diff --git a/distribution/tutorials/soap/membrane.sh b/distribution/tutorials/soap/membrane.sh index 859d1d1b9f..195dae51ec 100755 --- a/distribution/tutorials/soap/membrane.sh +++ b/distribution/tutorials/soap/membrane.sh @@ -1,86 +1,21 @@ #!/bin/sh - -# JAVA_OPTS (optional) JVM options applied to every Membrane invocation. -# Use this for memory tuning and required system properties. - -required_version="21" - -start() { - membrane_home="$1" - export CLASSPATH="$membrane_home/conf:$membrane_home/lib/*" - java $JAVA_OPTS -cp "$CLASSPATH" com.predic8.membrane.core.cli.RouterCLI -c proxies.xml - if [ $? -ne 0 ]; then - echo "Membrane terminated!" - echo "MEMBRANE_HOME: $membrane_home" - echo "CLASSPATH: $CLASSPATH" - fi -} - -find_membrane_directory() { - candidate=${MEMBRANE_HOME:-$membrane_home} - if [ -n "$candidate" ]; then - echo "$candidate" - return 0 - fi - - current="$1" - - while [ "$current" != "/" ]; do - if [ -f "$current/LICENSE.txt" ]; then - echo "$current" - return 0 - fi - current=$(dirname "$current") - done - - return 1 -} - -start_membrane() { - membrane_home=$(find_membrane_directory "$(pwd)") - if [ $? -eq 0 ]; then - start "$membrane_home" - else - echo "Could not start Membrane. Ensure the directory structure is correct." - fi -} - -if ! ( _test=test && _="${_test#t}" ) >/dev/null 2>&1; then - echo "WARNING: Shell does not support parameter expansion. Java version check disabled!" >&2 - echo " Please ensure Java $required_version is installed." >&2 - start_membrane - exit 0 -fi - -if ! command -v java >/dev/null 2>&1; then - echo "Java is not installed. Membrane needs at least Java $required_version." - exit 1 -fi - -version_line=$(java -version 2>&1 | grep "version" | head -n 1) - -if [ -z "$version_line" ]; then - echo "WARNING: Could not determine Java version. Make sure Java version is at least $required_version. Proceeding anyway..." - start_membrane - exit 0 -fi - -full_version=${version_line#*version \"} -full_version=${full_version%%\"*} -current_version=${full_version%%.*} - -case "$current_version" in - ''|*[!0-9]*) - echo "WARNING: Could not parse Java version. Make sure Java version is at least $required_version. Proceeding anyway..." - start_membrane - exit 0 - ;; -esac - -if [ "$current_version" -ge "$required_version" ]; then - start_membrane - exit 0 -else - echo "Java version mismatch: Required=$required_version, Installed=$full_version" - exit 1 -fi \ No newline at end of file +# Default: ./proxies.xml (next to this script); fallback -> $MEMBRANE_HOME/conf/proxies.xml +# JAVA_OPTS: relative -D paths are auto-resolved against $MEMBRANE_HOME (absolute/URI unchanged). +# Examples: +# export JAVA_OPTS='-Dlog4j.configurationFile=examples/logging/access/log4j2_access.xml' +# export JAVA_OPTS='-Dlog4j.configurationFile=/abs/path/log4j2.xml' + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +dir="$SCRIPT_DIR" +while [ "$dir" != "/" ]; do + if [ -f "$dir/LICENSE.txt" ] && [ -f "$dir/scripts/run-membrane.sh" ]; then + export MEMBRANE_HOME="$dir" + export MEMBRANE_CALLER_DIR="$SCRIPT_DIR" + exec sh "$dir/scripts/run-membrane.sh" "$@" + fi + dir=$(dirname "$dir") +done + +echo "Could not locate Membrane root. Ensure directory structure is correct." >&2 +exit 1 \ No newline at end of file From 6cd2ac306bd0078a1ff07465bf53840fc8fd08ac Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 8 Jan 2026 21:36:10 +0100 Subject: [PATCH 04/15] refactor(tests): clean up SOAP test logic and streamline method naming - Removed unused `.body()` setups in `WSDLRewriterTutorialTest`. - Simplified test method names in `WSDLInterceptorTest` for clarity and consistency. --- .../core/interceptor/WSDLInterceptorTest.java | 18 +++++++++--------- .../soap/WSDLRewriterTutorialTest.java | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java index bac66e77b9..8f65e964a2 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java @@ -37,7 +37,7 @@ public class WSDLInterceptorTest { private WSDLInterceptor interceptor; @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { exc = new Exchange(new FakeHttpHandler(3011)); exc.setRequest(get("/axis2/services/BLZService?wsdl").build()); exc.setResponse(ok() @@ -58,7 +58,7 @@ public void setUp() throws Exception { * */ @Test - void testProtocolSet() throws Exception { + void protocolSet() throws Exception { interceptor.setProtocol("https"); assertEquals(CONTINUE, interceptor.handleResponse(exc)); @@ -70,7 +70,7 @@ void testProtocolSet() throws Exception { } @Test - void testProtocolDefault() throws Exception { + void protocolDefault() throws Exception { assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertTrue(getLocationAttributeFor( @@ -82,7 +82,7 @@ void testProtocolDefault() throws Exception { } @Test - void testPortEmpty() throws Exception { + void portEmpty() throws Exception { interceptor.setPort(""); assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertFalse(matchSoap11(".*:80.*")); @@ -91,7 +91,7 @@ void testPortEmpty() throws Exception { } @Test - void testPortDefault() throws Exception { + void portDefault() throws Exception { assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertTrue(matchSoap11(".*:3011.*")); assertTrue(matchSoap12(".*:3011.*")); @@ -99,8 +99,8 @@ void testPortDefault() throws Exception { } @Test - void testPortSet() throws Exception { - interceptor.setPort("2000"); + void portSet() throws Exception { + interceptor.setPort(2000); assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertTrue(matchSoap11(".*:2000.*")); assertTrue(matchSoap12(".*:2000.*")); @@ -108,7 +108,7 @@ void testPortSet() throws Exception { } @Test - void testHostSet() throws Exception { + void hostSet() throws Exception { interceptor.setHost("abc.com"); assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertTrue(matchSoap11("http://abc.com.*")); @@ -117,7 +117,7 @@ void testHostSet() throws Exception { } @Test - void testHostDefault() throws Exception { + void hostDefault() throws Exception { assertEquals(CONTINUE, interceptor.handleResponse(exc)); assertTrue(matchSoap11("http://thomas-bayer.com.*")); assertTrue(matchSoap12("http://thomas-bayer.com.*")); diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java index 84beec5816..380ccaec92 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java @@ -63,7 +63,6 @@ void soapCall() throws IOException { void webServiceExplorer() { // @formatter:off given() - .body("Invalid") .when() .get("http://localhost:2000/my-service") .then() From aa83d00d9b4edb62ef9527c1cb3c0948ac23a381 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Fri, 9 Jan 2026 12:25:20 +0100 Subject: [PATCH 05/15] add: WSDL message validation tutorial with example XML - Introduced `40-WSDL-Message-Validation.yaml` for demonstrating WSDL validation in SOAP proxies. - Added `invalid-city.soap.xml` as a sample invalid SOAP request payload for tutorial testing. --- .../tutorials/data/invalid-city.soap.xml | 7 +++++++ .../soap/40-WSDL-Message-Validation.yaml | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 distribution/tutorials/data/invalid-city.soap.xml create mode 100644 distribution/tutorials/soap/40-WSDL-Message-Validation.yaml diff --git a/distribution/tutorials/data/invalid-city.soap.xml b/distribution/tutorials/data/invalid-city.soap.xml new file mode 100644 index 0000000000..cd51f65525 --- /dev/null +++ b/distribution/tutorials/data/invalid-city.soap.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml b/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml new file mode 100644 index 0000000000..ecfe28fd41 --- /dev/null +++ b/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# Tutorial: WSDL Message Validation +# +# +# 1.) Start Membrane +# +# 2.) Send request with invalid XML +# curl -d @../data/invalid-city.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service +# Response should contain a SOAP Fault + +soapProxy: + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl + flow: + - validator: {} From 1881d4f26d57438bd3b4ccfd52be091019a83a18 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Fri, 9 Jan 2026 13:25:50 +0100 Subject: [PATCH 06/15] add: `WSDLMessageValidationTutorialTest` to validate SOAP messages against WSDL constraints - Added a new test class extending `AbstractCityServiceTest` for validating WSDL message conformance with SOAP requests. - Utilizes `40-WSDL-Message-Validation.yaml` and `invalid-city.soap.xml` for testing invalid scenarios. --- .../WSDLMessageValidationTutorialTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java new file mode 100644 index 0000000000..1bd46ce792 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java @@ -0,0 +1,42 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +import org.junit.jupiter.api.*; + +import java.io.*; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +public class WSDLMessageValidationTutorialTest extends AbstractCityServiceTest { + + @Override + protected String getTutorialYaml() { + return "40-WSDL-Message-Validation.yaml"; + } + + @Override + @Test + void soapCall() throws IOException { + given() + // File is read from FS use the same file as the user + .body(readFileFromBaseDir("../data/invalid-city.soap.xml")) + .when() + .post("http://localhost:2000/city-service") + .then() + .body("Envelope.Body.Fault.faultstring", equalTo("WSDL message validation failed")); + } +} From a17d03f6f3437e090c2be02d6bbed06077bd5821 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 10 Jan 2026 08:22:02 +0100 Subject: [PATCH 07/15] refactor(soap-proxy): enhance WSDL handling, add `getProxyEndpointName`, and improve test coverage - Introduced `getProxyEndpointName` for extracting endpoint names from proxy paths. - Added `TestableSOAPProxy` and new test cases to validate WSDL path rewriting logic. - Simplified `addWSDLInterceptor` and `setPathRewriter` methods for improved readability. - Streamlined formatting and code consistency in `SOAPProxyTest`. --- .../membrane/core/proxies/SOAPProxy.java | 52 ++++++++++++------- .../membrane/core/proxies/SOAPProxyTest.java | 31 ++++++++--- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java index 62a80a69f7..33192f708c 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java @@ -18,7 +18,7 @@ import com.predic8.membrane.core.config.security.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.rewrite.*; -import com.predic8.membrane.core.interceptor.schemavalidation.ValidatorInterceptor; +import com.predic8.membrane.core.interceptor.schemavalidation.*; import com.predic8.membrane.core.interceptor.server.*; import com.predic8.membrane.core.interceptor.soap.*; import com.predic8.membrane.core.openapi.util.*; @@ -87,8 +87,8 @@ public void init() { configureFromWSDL(); super.init(); // Must be called last! Otherwise, SSL will not be configured! - for(Interceptor interceptor: interceptors) { - if(interceptor instanceof WSDLPublisherInterceptor wpi) { + for (Interceptor interceptor : interceptors) { + if (interceptor instanceof WSDLPublisherInterceptor wpi) { wpi.setSoapProxy(this); } else if (interceptor instanceof ValidatorInterceptor vi) { vi.setSoapProxy(this); @@ -273,8 +273,7 @@ private static Port getPortByNamespace(List ports, String namespace) { private void addWSDLInterceptor() { if (getFirstInterceptorOfType(WSDLInterceptor.class).isEmpty()) { - WSDLInterceptor wsdlInterceptor = new WSDLInterceptor(); - interceptors.addFirst(wsdlInterceptor); + interceptors.addFirst(new WSDLInterceptor()); } } @@ -290,23 +289,40 @@ private void renameMe() { } final String keyPath = key.getPath(); - final String name = getReplacementName(keyPath); - wsdlInterceptor.get().setPathRewriter(path2 -> { + wsdlInterceptor.get().setPathRewriter(path -> { try { - if (path2.contains("://")) { - return new URL(new URL(path2), keyPath).toString(); - } else { - Matcher m = relativePathPattern.matcher(path2); - return m.replaceAll("./" + name + "?"); + if (path.contains("://")) { + return new URL(new URL(path), keyPath).toString(); } + var replacementName = getProxyEndpointName(keyPath); + return rewriteRelativeWsdlPath(path, replacementName); } catch (MalformedURLException e) { - log.error("Cannot parse URL {}", path2); + log.error("Cannot parse URL {}", path); } - return path2; + return path; }); } - private @NotNull String getReplacementName(String keyPath) { + private static String rewriteRelativeWsdlPath(String path, String replacementName) { + return relativePathPattern.matcher(path).replaceAll("./" + replacementName + "?"); + } + + /** + * Extracts the proxy's published endpoint name from the configured proxy path. + * + *

The returned value is typically the last path segment of the proxy key path + * and is used when rewriting relative WSDL references so that they point to the + * gateway instead of the backend service.

+ * + *

For example, if the proxy key path is {@code "/my-service"}, this method + * returns {@code "my-service"}.

+ * + * @param keyPath the path of the proxy key, usually taken from the WSDL address or + * from the explicitly configured proxy path + * @return the endpoint name that should appear in rewritten WSDL URLs + * @throws RuntimeException if the given path cannot be parsed as a URI + */ + @NotNull String getProxyEndpointName(String keyPath) { try { return URLUtil.getName(router.getConfiguration().getUriFactory(), keyPath); } catch (URISyntaxException e) { @@ -316,7 +332,7 @@ private void renameMe() { } private void addWebServiceExplorer() { - WebServiceExplorerInterceptor sui = new WebServiceExplorerInterceptor(); + var sui = new WebServiceExplorerInterceptor(); sui.setWsdl(wsdl); sui.setPortName(portName); interceptors.addFirst(sui); @@ -326,10 +342,10 @@ private void addWSDLPublisherInterceptor() { if (hasWSDLPublisherInterceptor()) return; - WSDLPublisherInterceptor wp = new WSDLPublisherInterceptor(); + var wp = new WSDLPublisherInterceptor(); wp.setWsdl(wsdl); wp.init(router); - interceptors.addLast(wp); + interceptors.add(wp); } private boolean hasWSDLPublisherInterceptor() { diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java index 74a777c369..ff2556b787 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java @@ -93,12 +93,12 @@ void parseWSDLWithMultipleServicesForAGivenServiceB() throws Exception { // @formatter: off given().when() - .body(OpenAPITestUtils.getResourceAsStream(this, "/soap-sample/soap-request-bonn.xml")) - .post("http://localhost:2000/city-service") - .then() - .log().ifValidationFails(ALL) - .statusCode(200) - .contentType(TEXT_XML); + .body(OpenAPITestUtils.getResourceAsStream(this, "/soap-sample/soap-request-bonn.xml")) + .post("http://localhost:2000/city-service") + .then() + .log().ifValidationFails(ALL) + .statusCode(200) + .contentType(TEXT_XML); // @formatter: on } @@ -107,7 +107,24 @@ void parseWSDLWithMultipleServicesForAWrongService() { proxy.setServiceName("WrongService"); proxy.setWsdl("classpath:/ws/cities-2-services.wsdl"); - assertThrows(IllegalArgumentException.class, () -> {router.add(proxy); router.start(); + assertThrows(IllegalArgumentException.class, () -> { + router.add(proxy); + router.start(); }); } + + static class TestableSOAPProxy extends SOAPProxy { + TestableSOAPProxy(Router router) { + this.router = router; + } + } + + @Test + void getProxyEndpointName() throws IOException { + SOAPProxy sp = new TestableSOAPProxy(new DummyTestRouter()); + sp.setWsdl("http://localhost/a-service?wsdl"); + assertEquals("my-service", sp.getProxyEndpointName("/my-service")); + assertEquals("orders", sp.getProxyEndpointName("/api/orders")); + assertEquals("orders", sp.getProxyEndpointName("/api/orders/")); + } } \ No newline at end of file From 433d77bca4fea94573562a472e0e9469d7924d2c Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 10 Jan 2026 21:38:48 +0100 Subject: [PATCH 08/15] refactor(soap-proxy): modularize WSDL logic, add utilities, and improve test coverage - Extracted WSDL-related logic into `WSDLUtil` and introduced new helper methods like `rewriteRelativeWsdlPath` for better modularity. - Enhanced `SOAPProxy` initialization to streamline and centralize WSDL interceptor setup. - Added `WSDLUtilTest` for unit testing path rewriting logic. - Improved test coverage across WSDL-related test classes by adding and modifying cases in `SOAPProxyTest` and creating additional tests for `WSDLInterceptor`. - Simplified and refactored existing methods to improve readability and maintainability. --- .../predic8/membrane/core/http/Request.java | 5 +- .../interceptor/RelocatingInterceptor.java | 11 +- .../core/interceptor/WSDLInterceptor.java | 23 ++++ .../server/WSDLPublisherInterceptor.java | 41 ++++--- .../membrane/core/proxies/SOAPProxy.java | 108 ++++-------------- .../predic8/membrane/core/util/URLUtil.java | 15 ++- .../membrane/core/util/soap/WSDLUtil.java | 59 ++++++++++ .../membrane/core/ws/relocator/Relocator.java | 27 +---- .../core/interceptor/WSDLInterceptorTest.java | 20 ++-- .../membrane/core/proxies/SOAPProxyTest.java | 16 +-- ...SOAPProxyWSDLPublisherInterceptorTest.java | 13 +-- .../membrane/core/util/URLUtilTest.java | 9 ++ .../membrane/core/util/soap/WSDLUtilTest.java | 14 +++ .../soap/AbstractCityServiceTest.java | 14 +++ .../soap/SampleSOAPServiceTutorialTest.java | 14 +++ .../WSDLMessageValidationTutorialTest.java | 14 ++- 16 files changed, 222 insertions(+), 181 deletions(-) create mode 100644 core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java create mode 100644 core/src/test/java/com/predic8/membrane/core/util/soap/WSDLUtilTest.java diff --git a/core/src/main/java/com/predic8/membrane/core/http/Request.java b/core/src/main/java/com/predic8/membrane/core/http/Request.java index 3f3724ee77..07d95283ca 100644 --- a/core/src/main/java/com/predic8/membrane/core/http/Request.java +++ b/core/src/main/java/com/predic8/membrane/core/http/Request.java @@ -161,7 +161,6 @@ public boolean shouldNotContainBody() { return header.getContentLength() == 0; return header.getFirstValue(TRANSFER_ENCODING) == null; } - return false; } @@ -185,7 +184,7 @@ public int estimateHeapSize() { @Override public T createSnapshot(Runnable bodyUpdatedCallback, BodyCollectingMessageObserver.Strategy strategy, long limit) { - Request result = createMessageSnapshot(new Request(), bodyUpdatedCallback, strategy, limit); + var result = createMessageSnapshot(new Request(), bodyUpdatedCallback, strategy, limit); result.setUri(getUri()); result.setMethod(getMethod()); return (T) result; @@ -195,7 +194,7 @@ public final void writeSTOMP(OutputStream out, boolean retainBody) throws IOExce out.write(getMethod().getBytes(UTF_8)); out.write(10); for (HeaderField hf : header.getAllHeaderFields()) - out.write((hf.getHeaderName().toString() + ":" + hf.getValue() + "\n").getBytes(UTF_8)); + out.write(("%s:%s\n".formatted(hf.getHeaderName().toString(), hf.getValue())).getBytes(UTF_8)); out.write(10); body.write(new PlainBodyTransferer(out), retainBody); } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java index b33d826a3c..94dc94221e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java @@ -43,16 +43,11 @@ public Outcome handleResponse(Exchange exc) { return CONTINUE; } - if (!wasGetRequest(exc)) { + if (! exc.getRequest().isGETRequest()) { log.debug("{} HTTP method wasn't GET: No relocating done!",name); return CONTINUE; } - if (!hasContent(exc)) { - log.debug("{} No Content: No relocating done!",name); - return CONTINUE; - } - if (!exc.getResponse().isXML()) { log.debug("{} Body contains no XML: No relocating done!",name); return CONTINUE; @@ -78,10 +73,6 @@ private boolean hasContent(Exchange exc) { return exc.getResponse().getHeader().getContentType() != null; } - private boolean wasGetRequest(Exchange exc) { - return Request.METHOD_GET.equals(exc.getRequest().getMethod()); - } - protected int getLocationPort(Exchange exc) { if ("".equals(port)) { return -1; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 310f86818e..789ed9d36b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -18,6 +18,7 @@ import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.transport.http.*; +import com.predic8.membrane.core.util.*; import com.predic8.membrane.core.ws.relocator.*; import org.jetbrains.annotations.*; import org.slf4j.*; @@ -25,11 +26,13 @@ import javax.xml.namespace.*; import java.io.*; import java.net.*; +import java.util.regex.*; import static com.predic8.membrane.core.Constants.*; import static com.predic8.membrane.core.http.Header.*; import static com.predic8.membrane.core.http.Request.*; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.Set.*; +import static com.predic8.membrane.core.util.soap.WSDLUtil.rewriteRelativeWsdlPath; import static java.nio.charset.StandardCharsets.*; /** @@ -60,6 +63,26 @@ public void init() { hc = router.getHttpClientFactory().createClient(null); } + public void setPathRewriterOnWSDLInterceptor(String keypath) { + if (keypath == null) + return; + setPathRewriter(generatePathRewriter(keypath)); + } + + Relocator.@NotNull PathRewriter generatePathRewriter(String keypath) { + return path -> { + try { + if (path.contains("://")) { + return new URL(new URL(path), keypath).toString(); + } + return rewriteRelativeWsdlPath(path, URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), keypath)); + } catch (URISyntaxException | MalformedURLException e) { + log.error("Cannot parse URL {}", path); + } + return path; + }; + } + @Override protected void rewrite(Exchange exc) throws Exception { log.debug("Changing endpoint address in WSDL"); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java index 294a3d8d35..59e1ecb588 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/server/WSDLPublisherInterceptor.java @@ -29,9 +29,8 @@ import static com.predic8.membrane.core.exceptions.ProblemDetails.*; import static com.predic8.membrane.core.http.MimeType.*; import static com.predic8.membrane.core.http.Response.*; -import static com.predic8.membrane.core.http.Response.notFound; import static com.predic8.membrane.core.interceptor.Outcome.*; -import static com.predic8.membrane.core.resolver.ResolverMap.combine; +import static com.predic8.membrane.core.resolver.ResolverMap.*; /** * @description

@@ -48,6 +47,8 @@ public class WSDLPublisherInterceptor extends AbstractInterceptor { private SOAPProxy soapProxy; + private String wsdl; + public WSDLPublisherInterceptor() { name = "wsdl publisher"; } @@ -86,7 +87,7 @@ public String rewrite(String path) { path = Integer.toString(n); } } - path = "./" + URLUtil.getName(router.getConfiguration().getUriFactory(), exc.getDestinations().getFirst()) + "?xsd=" + path; + path = "./" + URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), exc.getDestinations().getFirst()) + "?xsd=" + path; } catch (Exception e) { throw new RuntimeException(e); } @@ -94,10 +95,23 @@ public String rewrite(String path) { } } + @Override + public void init() { + super.init(); + + webServerInterceptor = new WebServerInterceptor(); + webServerInterceptor.init(router); + + // inherit wsdl="..." from SoapProxy + if (wsdl != null) + return; + getWSDLFromEmbeddingSOAPProxy(); + } + @GuardedBy("paths") - private final HashMap paths = new HashMap<>(); + private final Map paths = new HashMap<>(); @GuardedBy("paths") - private final HashMap paths_reverse = new HashMap<>(); + private final Map paths_reverse = new HashMap<>(); @GuardedBy("paths") private final Queue documents_to_process = new LinkedList<>(); @@ -123,8 +137,6 @@ private void processDocuments(Exchange exc) { } } - private String wsdl; - public String getWsdl() { return wsdl; } @@ -144,19 +156,6 @@ public void setWsdl(String wsdl) { } } - @Override - public void init() { - super.init(); - - webServerInterceptor = new WebServerInterceptor(); - webServerInterceptor.init(router); - - // inherit wsdl="..." from SoapProxy - if (wsdl != null) - return; - getWSDLFromEmbeddingSOAPProxy(); - } - private void getWSDLFromEmbeddingSOAPProxy() { if (soapProxy == null) { throw new ConfigurationException(" can only be used within a or needs to declare "); @@ -171,7 +170,7 @@ public Outcome handleRequest(final Exchange exc) { return handleRequestInternal(exc); } catch (Exception e) { log.error("", e); - internal(router.getConfiguration().isProduction(),getDisplayName()) + internal(router.getConfiguration().isProduction(), getDisplayName()) .detail("Could not return WSDL document!") .exception(e) .buildAndSetResponse(exc); diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java index 33192f708c..ce8fcdc340 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java @@ -26,17 +26,17 @@ import com.predic8.membrane.core.router.*; import com.predic8.membrane.core.transport.http.client.*; import com.predic8.membrane.core.util.*; +import com.predic8.membrane.core.util.soap.WSDLUtil; import com.predic8.wsdl.*; import org.apache.commons.lang3.*; import org.jetbrains.annotations.*; import org.slf4j.*; -import javax.xml.namespace.*; import java.net.*; import java.util.*; import java.util.regex.*; -import static com.predic8.membrane.core.Constants.*; +import static com.predic8.membrane.core.util.soap.WSDLUtil.*; /** * @description

@@ -60,7 +60,6 @@ public class SOAPProxy extends AbstractServiceProxy { private static final Logger log = LoggerFactory.getLogger(SOAPProxy.class.getName()); - private static final Pattern relativePathPattern = Pattern.compile("^./[^/?]*\\?"); // configuration attributes protected String wsdl; @@ -111,12 +110,12 @@ protected void configureFromWSDL() { prepareRouting(location); - // add interceptors (in reverse order) to position 0. - addWebServiceExplorer(); - addWSDLPublisherInterceptor(); - addWSDLInterceptor(); - renameMe(); - + // Add interceptors (in reverse order) cause each one calls List.addFirst. + // This is needed because there might be already a validator interceptor that must go last + addWebServiceExplorer(); // Will be last to validator + addWSDLPublisherInterceptor(); // Will be before WebServiceExploroer + var wsdlInterceptor = addAndGetWSDLInterceptor(); // WSDLInterceptor will be first + setPathRewriterOnWSDLInterceptor(wsdlInterceptor); } private Definitions parseWSDLOnly() { @@ -239,98 +238,35 @@ public static Port selectPort(List ports, String portName) { return port; throw new IllegalArgumentException("No port with name '" + portName + "' found."); } - return getPort(ports); - } - - private static Port getPort(List ports) { - Port port = getPortByNamespace(ports, WSDL_SOAP11_NS); - if (port == null) - port = getPortByNamespace(ports, WSDL_SOAP12_NS); - if (port == null) - throw new IllegalArgumentException("No SOAP/1.1 or SOAP/1.2 ports found in WSDL."); - return port; + return WSDLUtil.getPort(ports); } - private static Port getPortByNamespace(List ports, String namespace) { - for (Port port : ports) { - try { - if (port.getBinding() == null) - continue; - if (port.getBinding().getBinding() == null) - continue; - AbstractBinding binding = port.getBinding().getBinding(); - if (!"http://schemas.xmlsoap.org/soap/http".equals(binding.getProperty("transport"))) - continue; - if (!namespace.equals(((QName) binding.getElementName()).getNamespaceURI())) - continue; - return port; - } catch (Exception e) { - log.warn("Error inspecting WSDL port binding.", e); - } - } - return null; + private WSDLInterceptor addAndGetWSDLInterceptor() { + return getFirstInterceptorOfType(WSDLInterceptor.class) + .orElseGet(() -> { + var i = new WSDLInterceptor(); + interceptors.addFirst(i); + return i; + }); } - private void addWSDLInterceptor() { - if (getFirstInterceptorOfType(WSDLInterceptor.class).isEmpty()) { - interceptors.addFirst(new WSDLInterceptor()); - } - } - - private void renameMe() { + private void setPathRewriterOnWSDLInterceptor(WSDLInterceptor wsdlInterceptor) { if (key.getPath() == null) return; - - Optional wsdlInterceptor = getFirstInterceptorOfType(WSDLInterceptor.class); - - if (wsdlInterceptor.isEmpty()) { - log.warn("No wsdl interceptor set."); - return; - } - final String keyPath = key.getPath(); - wsdlInterceptor.get().setPathRewriter(path -> { + wsdlInterceptor.setPathRewriter(path -> { try { if (path.contains("://")) { return new URL(new URL(path), keyPath).toString(); } - var replacementName = getProxyEndpointName(keyPath); - return rewriteRelativeWsdlPath(path, replacementName); - } catch (MalformedURLException e) { - log.error("Cannot parse URL {}", path); + return rewriteRelativeWsdlPath(path, URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), keyPath)); + } catch (URISyntaxException | MalformedURLException e) { + log.error("Cannot parse URL {}",path); } return path; }); } - private static String rewriteRelativeWsdlPath(String path, String replacementName) { - return relativePathPattern.matcher(path).replaceAll("./" + replacementName + "?"); - } - - /** - * Extracts the proxy's published endpoint name from the configured proxy path. - * - *

The returned value is typically the last path segment of the proxy key path - * and is used when rewriting relative WSDL references so that they point to the - * gateway instead of the backend service.

- * - *

For example, if the proxy key path is {@code "/my-service"}, this method - * returns {@code "my-service"}.

- * - * @param keyPath the path of the proxy key, usually taken from the WSDL address or - * from the explicitly configured proxy path - * @return the endpoint name that should appear in rewritten WSDL URLs - * @throws RuntimeException if the given path cannot be parsed as a URI - */ - @NotNull String getProxyEndpointName(String keyPath) { - try { - return URLUtil.getName(router.getConfiguration().getUriFactory(), keyPath); - } catch (URISyntaxException e) { - log.error("Error parsing URL {}", keyPath, e); - throw new RuntimeException("Check!"); - } - } - private void addWebServiceExplorer() { var sui = new WebServiceExplorerInterceptor(); sui.setWsdl(wsdl); @@ -345,7 +281,7 @@ private void addWSDLPublisherInterceptor() { var wp = new WSDLPublisherInterceptor(); wp.setWsdl(wsdl); wp.init(router); - interceptors.add(wp); + interceptors.addFirst(wp); } private boolean hasWSDLPublisherInterceptor() { diff --git a/core/src/main/java/com/predic8/membrane/core/util/URLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/URLUtil.java index 41d97c8cd1..77c6495dca 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/URLUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/URLUtil.java @@ -33,9 +33,18 @@ public static String getPathQuery(URIFactory uriFactory, String uri) throws URIS return (path.isEmpty() ? "/" : path) + (query == null ? "" : "?" + query); } - public static String getName(URIFactory uriFactory, String uri) throws URISyntaxException { - URI u = uriFactory.create(uri); - String p = u.getPath(); + /** + * Extracts and returns the name component from the path of a URI. The name + * corresponds to the substring after the last '/' in the path. If no '/' is + * found, the entire path is returned. + * + * @param uriFactory An instance of {@code URIFactory} used to create the {@code URI} object. + * @param uri The URI string to process. + * @return The name component extracted from the URI's path. + * @throws URISyntaxException If the URI string is invalid and cannot be converted into a {@code URI}. + */ + public static String getNameComponent(URIFactory uriFactory, String uri) throws URISyntaxException { + var p = uriFactory.create(uri).getPath(); int i = p.lastIndexOf('/'); return i == -1 ? p : p.substring(i+1); } diff --git a/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java new file mode 100644 index 0000000000..a82f1d164f --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java @@ -0,0 +1,59 @@ +package com.predic8.membrane.core.util.soap; + +import com.predic8.membrane.core.proxies.*; +import com.predic8.wsdl.*; +import org.slf4j.*; + +import javax.xml.namespace.*; +import java.util.*; +import java.util.regex.*; + +import static com.predic8.membrane.core.Constants.*; + +public class WSDLUtil { + + private static final Logger log = LoggerFactory.getLogger(WSDLUtil.class.getName()); + + private static final Pattern relativePathPattern = Pattern.compile("^./[^/?]*\\?"); + + public static String rewriteRelativeWsdlPath(String path, String replacementName) { + return relativePathPattern.matcher(path).replaceAll("./%s?".formatted(replacementName)); + } + + /** + * Retrieves a SOAP port from the given list of ports. This method first attempts to find a + * port using the SOAP 1.1 namespace and, if not found, tries using the SOAP 1.2 namespace. + * + * @param ports the list of available ports to search for a SOAP/1.1 or SOAP/1.2 port + * @return the first matching SOAP port if found + * @throws IllegalArgumentException if no SOAP/1.1 or SOAP/1.2 port is found + */ + public static Port getPort(List ports) { + Port port = getPortByNamespace(ports, WSDL_SOAP11_NS); + if (port == null) + port = getPortByNamespace(ports, WSDL_SOAP12_NS); + if (port != null) + return port; + throw new IllegalArgumentException("No SOAP/1.1 or SOAP/1.2 ports found in WSDL."); + } + + private static Port getPortByNamespace(List ports, String namespace) { + for (Port port : ports) { + try { + if (port.getBinding() == null) + continue; + if (port.getBinding().getBinding() == null) + continue; + AbstractBinding binding = port.getBinding().getBinding(); + if (!"http://schemas.xmlsoap.org/soap/http".equals(binding.getProperty("transport"))) + continue; + if (!namespace.equals(((QName) binding.getElementName()).getNamespaceURI())) + continue; + return port; + } catch (Exception e) { + log.warn("Error inspecting WSDL port binding.", e); + } + } + return null; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/ws/relocator/Relocator.java b/core/src/main/java/com/predic8/membrane/core/ws/relocator/Relocator.java index 4898ba88f3..46e06a70c2 100644 --- a/core/src/main/java/com/predic8/membrane/core/ws/relocator/Relocator.java +++ b/core/src/main/java/com/predic8/membrane/core/ws/relocator/Relocator.java @@ -40,20 +40,10 @@ public class Relocator { private final XMLEventWriter writer; private final PathRewriter pathRewriter; - private Map relocatingAttributes = new HashMap<>(); + private final Map relocatingAttributes = new HashMap<>(); private boolean wsdlFound; - public Relocator(Writer w, String protocol, String host, int port, String contextPath, PathRewriter pathRewriter) - throws Exception { - this.writer = XMLOutputFactory.newInstance().createXMLEventWriter(w); - this.host = host; - this.port = port; - this.protocol = protocol; - this.contextPath = contextPath; - this.pathRewriter = pathRewriter; - } - public Relocator(OutputStreamWriter osw, String protocol, String host, int port, String contextPath, PathRewriter pathRewriter) throws Exception { this.writer = XMLOutputFactory.newInstance().createXMLEventWriter(osw); @@ -64,8 +54,7 @@ public Relocator(OutputStreamWriter osw, String protocol, String host, this.pathRewriter = pathRewriter; } - public static String getNewLocation(String addr, String protocol, - String host, int port, String contextPath) { + public static String getNewLocation(String addr, String protocol, String host, int port, String contextPath) { try { URL oldURL = new URL(addr); if (port == -1) { @@ -82,11 +71,6 @@ public static String getNewLocation(String addr, String protocol, return ""; } - @Deprecated - public void relocate(InputStreamReader isr) throws Exception { - relocate(XMLInputFactoryFactory.inputFactory().createXMLEventReader(isr)); - } - public void relocate(InputStream is) throws Exception { relocate(XMLInputFactoryFactory.inputFactory().createXMLEventReader(is)); } @@ -142,10 +126,6 @@ public Map getRelocatingAttributes() { return relocatingAttributes; } - public void setRelocatingAttributes(Map relocatingAttributes) { - this.relocatingAttributes = relocatingAttributes; - } - public interface PathRewriter { String rewrite(String path); } @@ -193,5 +173,4 @@ private static void close(XMLEventReader parser) { } catch (Exception ignore) { } } - -} +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java index 8f65e964a2..732b52e4c9 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java @@ -14,6 +14,7 @@ package com.predic8.membrane.core.interceptor; import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.router.*; import com.predic8.membrane.core.transport.http.*; import org.junit.jupiter.api.*; @@ -48,6 +49,7 @@ void setUp() throws Exception { exc.setOriginalHostHeader("thomas-bayer.com:80"); interceptor = new WSDLInterceptor(); + interceptor.init(new DummyTestRouter()); } /** @@ -61,9 +63,7 @@ void setUp() throws Exception { void protocolSet() throws Exception { interceptor.setProtocol("https"); assertEquals(CONTINUE, interceptor.handleResponse(exc)); - XMLEventReader parser = getParser(); - assertTrue(getLocationAttributeFor(getElement(parser, WSDL11_ADDRESS_SOAP11)).startsWith("https://")); assertTrue(getLocationAttributeFor(getElement(getParser(), WSDL11_ADDRESS_SOAP12)).startsWith("https://")); assertTrue(getLocationAttributeFor(getElement(getParser(), WSDL11_ADDRESS_HTTP)).startsWith("https://")); @@ -147,8 +147,7 @@ private boolean matchHttp(String pattern) throws Exception { return match(pattern, WSDL11_ADDRESS_HTTP); } - private boolean match(String pattern, QName addressElementName) - throws Exception { + private boolean match(String pattern, QName addressElementName) throws Exception { return Pattern .compile(pattern) .matcher( @@ -156,8 +155,7 @@ private boolean match(String pattern, QName addressElementName) addressElementName))).matches(); } - private StartElement getElement(XMLEventReader parser, QName qName) - throws XMLStreamException { + private StartElement getElement(XMLEventReader parser, QName qName) throws XMLStreamException { while (parser.hasNext()) { XMLEvent event = parser.nextEvent(); @@ -167,8 +165,14 @@ private StartElement getElement(XMLEventReader parser, QName qName) } } } - throw new RuntimeException("element " + qName - + " not found in response"); + throw new RuntimeException("element %s not found in response".formatted(qName)); } + @Test + void generatePathRewriter() { + var relocator = interceptor.generatePathRewriter("service-a"); + assertEquals("http://api.predic8.de/service-a", relocator.rewrite("http://api.predic8.de/service-b")); + assertEquals("http://api.predic8.de/service-a", relocator.rewrite("http://api.predic8.de/service-b?WSDL")); + assertEquals("./service-a?WSDL", relocator.rewrite("./service-b?WSDL")); + } } diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java index ff2556b787..ee9f99911e 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyTest.java @@ -18,6 +18,7 @@ import com.predic8.membrane.core.openapi.serviceproxy.*; import com.predic8.membrane.core.openapi.util.*; import com.predic8.membrane.core.router.*; +import com.predic8.membrane.core.util.*; import org.junit.jupiter.api.*; import java.io.*; @@ -112,19 +113,4 @@ void parseWSDLWithMultipleServicesForAWrongService() { router.start(); }); } - - static class TestableSOAPProxy extends SOAPProxy { - TestableSOAPProxy(Router router) { - this.router = router; - } - } - - @Test - void getProxyEndpointName() throws IOException { - SOAPProxy sp = new TestableSOAPProxy(new DummyTestRouter()); - sp.setWsdl("http://localhost/a-service?wsdl"); - assertEquals("my-service", sp.getProxyEndpointName("/my-service")); - assertEquals("orders", sp.getProxyEndpointName("/api/orders")); - assertEquals("orders", sp.getProxyEndpointName("/api/orders/")); - } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyWSDLPublisherInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyWSDLPublisherInterceptorTest.java index ca896fedc7..6a5cfdb499 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyWSDLPublisherInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/SOAPProxyWSDLPublisherInterceptorTest.java @@ -17,7 +17,7 @@ import com.predic8.membrane.core.router.*; import org.junit.jupiter.api.*; -import static com.predic8.membrane.test.TestUtil.getPathFromResource; +import static com.predic8.membrane.test.TestUtil.*; import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; @@ -40,10 +40,10 @@ static void teardown() { @Test void publisherOnly() throws Exception { SOAPProxy sp = new SOAPProxy() {{ - wsdl = getPathFromResource( "validation/ArticleService.wsdl"); + wsdl = getPathFromResource("validation/ArticleService.wsdl"); key = new ServiceProxyKey(2000); }}; - sp.setPath(new Path(false,"/articles")); + sp.setPath(new Path(false, "/articles")); router.add(sp); router.start(); downloadAndVerifyDocuments("localhost:2000"); @@ -54,8 +54,7 @@ private static void downloadAndVerifyDocuments(String host) { given() .get("http://localhost:2000/articles?wsdl") .then() - .body("definitions.service.port.address.@location", - equalTo("http://%s/articles".formatted(host))); + .body("definitions.service.port.address.@location", equalTo("http://%s/articles".formatted(host))); given() .get("http://localhost:2000/articles?xsd=1") @@ -76,6 +75,6 @@ private static void downloadAndVerifyDocuments(String host) { .get(("http://localhost:2000/articles?xsd=4")) .then() .body("definitions.complexType.@name",equalTo("MoneyType")); - // @formatter:off -} + // @formatter:on + } } diff --git a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java index 9f077b3b34..c74f4e02a5 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java @@ -70,4 +70,13 @@ void getPortFromURLTest() throws MalformedURLException { assertEquals(80, getPortFromURL(new URL("http://localhost"))); assertEquals(443, getPortFromURL(new URL("https://api.predic8.de"))); } + + @Test + void testGetName() throws Exception { + assertEquals("foo", URLUtil.getNameComponent(new URIFactory(), "foo")); + assertEquals("foo", URLUtil.getNameComponent(new URIFactory(), "/foo")); + assertEquals("bar", URLUtil.getNameComponent(new URIFactory(), "/foo/bar")); + assertEquals("bar", URLUtil.getNameComponent(new URIFactory(), "foo/bar")); + assertEquals("", URLUtil.getNameComponent(new URIFactory(), "foo/bar/")); + } } diff --git a/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLUtilTest.java new file mode 100644 index 0000000000..60cfc5de69 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLUtilTest.java @@ -0,0 +1,14 @@ +package com.predic8.membrane.core.util.soap; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +class WSDLUtilTest { + + @Test + void testRewriteRelativeWsdlPath() { + assertEquals("./a-service?wsdl", WSDLUtil.rewriteRelativeWsdlPath("./city-service?wsdl", "a-service")); + } + +} \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java index b9075b29f5..0d3bc5b39a 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.tutorials.soap; import org.junit.jupiter.api.*; diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java index c9ad97644d..3b1b803f04 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.tutorials.soap; public class SampleSOAPServiceTutorialTest extends AbstractCityServiceTest { diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java index 1bd46ce792..949e2f3f05 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java @@ -18,8 +18,9 @@ import java.io.*; -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; +import static com.predic8.membrane.core.http.MimeType.*; +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; public class WSDLMessageValidationTutorialTest extends AbstractCityServiceTest { @@ -31,12 +32,17 @@ protected String getTutorialYaml() { @Override @Test void soapCall() throws IOException { + // @formatter:off given() - // File is read from FS use the same file as the user + // File is read from FS uses the same file as the user .body(readFileFromBaseDir("../data/invalid-city.soap.xml")) + .contentType(TEXT_XML_UTF8) .when() .post("http://localhost:2000/city-service") .then() - .body("Envelope.Body.Fault.faultstring", equalTo("WSDL message validation failed")); + .log().body() + .body("Envelope.Body.Fault.faultstring", equalTo("WSDL message validation failed")) + .body(containsString("INVALID") ); + // @formatter:on } } From c7041c1984328640164871257df6e0ae787af9b4 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 10 Jan 2026 21:40:54 +0100 Subject: [PATCH 09/15] refactor(wsdl-interceptor): clean up annotations, remove unused method, and improve test readability - Removed redundant `hasContent` method from `RelocatingInterceptor`. - Simplified `@MCElement` annotation in `WSDLInterceptor` by removing `excludeFromFlow` attribute. - Improved readability and structure of assertions in `URLUtilTest`. --- .../core/interceptor/RelocatingInterceptor.java | 4 ---- .../membrane/core/interceptor/WSDLInterceptor.java | 2 +- .../com/predic8/membrane/core/util/URLUtilTest.java | 12 ++++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java index 94dc94221e..80f7031664 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java @@ -69,10 +69,6 @@ public Outcome handleResponse(Exchange exc) { abstract void rewrite(Exchange exc) throws Exception; - private boolean hasContent(Exchange exc) { - return exc.getResponse().getHeader().getContentType() != null; - } - protected int getLocationPort(Exchange exc) { if ("".equals(port)) { return -1; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 789ed9d36b..7dd49e7d5e 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -39,7 +39,7 @@ * @description

The wsdlRewriter rewrites endpoint addresses of services and XML Schema locations in WSDL documents.

* @topic 5. Web Services with SOAP and WSDL */ -@MCElement(name = "wsdlRewriter", excludeFromFlow = false) +@MCElement(name = "wsdlRewriter") public class WSDLInterceptor extends RelocatingInterceptor { private final static Logger log = LoggerFactory.getLogger(WSDLInterceptor.class.getName()); diff --git a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java index c74f4e02a5..9a8436deff 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java @@ -28,12 +28,12 @@ public class URLUtilTest { @Test void host() { - assertEquals(getHost("internal:a"), "a"); - assertEquals(getHost("internal://a"), "a"); - assertEquals(getHost("a"), "a"); - assertEquals(getHost("a/b"), "a"); - assertEquals(getHost("internal:a/b"), "a"); - assertEquals(getHost("internal://a/b"), "a"); + assertEquals("a", getHost("internal:a")); + assertEquals("a", getHost("internal://a")); + assertEquals("a", getHost("a")); + assertEquals("a", getHost("a/b")); + assertEquals("a", getHost("internal:a/b")); + assertEquals("a", getHost("internal://a/b")); } @Test From 5047eb16abc79187b652a7b1e74b32ed68fee036 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sat, 10 Jan 2026 22:07:59 +0100 Subject: [PATCH 10/15] refactor(soap-proxy): remove unused method, fix typos, and improve interceptor setup - Deleted the redundant `setPathRewriterOnWSDLInterceptor` method from `SOAPProxy`. - Corrected a typo in method comments and variable usage (`WebServiceExploroer` -> `WebServiceExplorer`). - Enhanced `setPathRewriter` call to streamline WSDL interceptor setup. - Updated `URLUtilTest` to expand and clarify `getNameComponent` behavior. - Refined `WSDLUtil` regex usage for better consistency. --- .../core/interceptor/WSDLInterceptor.java | 2 +- .../membrane/core/proxies/SOAPProxy.java | 23 ++----------------- .../membrane/core/util/soap/WSDLUtil.java | 7 ++++-- .../membrane/core/util/URLUtilTest.java | 15 +++++++----- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 7dd49e7d5e..3276144f36 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -77,7 +77,7 @@ public void setPathRewriterOnWSDLInterceptor(String keypath) { } return rewriteRelativeWsdlPath(path, URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), keypath)); } catch (URISyntaxException | MalformedURLException e) { - log.error("Cannot parse URL {}", path); + log.error("Cannot parse URL {} - {}", path,e.getMessage()); } return path; }; diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java index ce8fcdc340..5ad4f46080 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java @@ -36,8 +36,6 @@ import java.util.*; import java.util.regex.*; -import static com.predic8.membrane.core.util.soap.WSDLUtil.*; - /** * @description

* A SOAP proxy automatically configures itself using a WSDL description. It reads the WSDL to extract: @@ -113,9 +111,9 @@ protected void configureFromWSDL() { // Add interceptors (in reverse order) cause each one calls List.addFirst. // This is needed because there might be already a validator interceptor that must go last addWebServiceExplorer(); // Will be last to validator - addWSDLPublisherInterceptor(); // Will be before WebServiceExploroer + addWSDLPublisherInterceptor(); // Will be before WebServiceExplorer var wsdlInterceptor = addAndGetWSDLInterceptor(); // WSDLInterceptor will be first - setPathRewriterOnWSDLInterceptor(wsdlInterceptor); + wsdlInterceptor.setPathRewriterOnWSDLInterceptor(key.getPath()); } private Definitions parseWSDLOnly() { @@ -250,23 +248,6 @@ private WSDLInterceptor addAndGetWSDLInterceptor() { }); } - private void setPathRewriterOnWSDLInterceptor(WSDLInterceptor wsdlInterceptor) { - if (key.getPath() == null) - return; - final String keyPath = key.getPath(); - wsdlInterceptor.setPathRewriter(path -> { - try { - if (path.contains("://")) { - return new URL(new URL(path), keyPath).toString(); - } - return rewriteRelativeWsdlPath(path, URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), keyPath)); - } catch (URISyntaxException | MalformedURLException e) { - log.error("Cannot parse URL {}",path); - } - return path; - }); - } - private void addWebServiceExplorer() { var sui = new WebServiceExplorerInterceptor(); sui.setWsdl(wsdl); diff --git a/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java index a82f1d164f..32df9da936 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java @@ -9,15 +9,18 @@ import java.util.regex.*; import static com.predic8.membrane.core.Constants.*; +import static java.util.regex.Matcher.quoteReplacement; public class WSDLUtil { private static final Logger log = LoggerFactory.getLogger(WSDLUtil.class.getName()); - private static final Pattern relativePathPattern = Pattern.compile("^./[^/?]*\\?"); + private static final Pattern relativePathPattern = Pattern.compile("^\\./[^/?]*\\?"); public static String rewriteRelativeWsdlPath(String path, String replacementName) { - return relativePathPattern.matcher(path).replaceAll("./%s?".formatted(replacementName)); + return relativePathPattern + .matcher(path) + .replaceAll(quoteReplacement("./%s?".formatted(replacementName))); } /** diff --git a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java index 9a8436deff..1bd1dc3136 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/URLUtilTest.java @@ -22,6 +22,7 @@ import static com.predic8.membrane.core.util.URLParamUtil.DuplicateKeyOrInvalidFormStrategy.*; import static com.predic8.membrane.core.util.URLParamUtil.*; import static com.predic8.membrane.core.util.URLUtil.*; +import static com.predic8.membrane.core.util.URLUtil.getNameComponent; import static org.junit.jupiter.api.Assertions.*; public class URLUtilTest { @@ -72,11 +73,13 @@ void getPortFromURLTest() throws MalformedURLException { } @Test - void testGetName() throws Exception { - assertEquals("foo", URLUtil.getNameComponent(new URIFactory(), "foo")); - assertEquals("foo", URLUtil.getNameComponent(new URIFactory(), "/foo")); - assertEquals("bar", URLUtil.getNameComponent(new URIFactory(), "/foo/bar")); - assertEquals("bar", URLUtil.getNameComponent(new URIFactory(), "foo/bar")); - assertEquals("", URLUtil.getNameComponent(new URIFactory(), "foo/bar/")); + void testGetNameComponent() throws Exception { + assertEquals("", getNameComponent(new URIFactory(), "")); + assertEquals("", getNameComponent(new URIFactory(), "/")); + assertEquals("foo", getNameComponent(new URIFactory(), "foo")); + assertEquals("foo", getNameComponent(new URIFactory(), "/foo")); + assertEquals("bar", getNameComponent(new URIFactory(), "/foo/bar")); + assertEquals("bar", getNameComponent(new URIFactory(), "foo/bar")); + assertEquals("", getNameComponent(new URIFactory(), "foo/bar/")); } } From 2921d8af14e1e75220e0eb9cc7d2679927181b09 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 11 Jan 2026 16:23:17 +0100 Subject: [PATCH 11/15] feat(soap-proxy): add manual SOAP proxy tutorial, enhance WSDL handling, and improve test coverage - Introduced `90-Manual-SOAPProxy.yaml` for setting up and testing manual SOAP proxies. - Added `ManualSOAPProxyTutorialTest` to validate SOAP proxy setup and ensure WSDL compliance. - Enhanced `moveToFirstPosition` utility for efficient WSDL interceptor addition and management. - Updated multiple YAML tutorials and tests to align with new SOAP proxy configuration. - Added `normalizeCRLF` utility in `StringTestUtil` for handling CRLF line endings in HTTP messages. --- .../core/interceptor/InterceptorUtil.java | 45 +++++++++++++++++- .../core/interceptor/WSDLInterceptor.java | 3 +- .../membrane/core/proxies/SOAPProxy.java | 9 ++-- .../membrane/core/http/ResponseTest.java | 31 ++++++++++-- .../membrane/core/util/StringTestUtil.java | 15 ++++++ .../soap/AbstractCityServiceTest.java | 2 +- .../soap/ManualSOAPProxyTutorialTest.java | 47 +++++++++++++++++++ .../soap/WSDLRewriterTutorialTest.java | 2 +- .../{city-request.soap.xml => city.soap.xml} | 0 .../soap/10-Sample-SOAP-Service.yaml | 4 +- distribution/tutorials/soap/20-SOAPProxy.yaml | 6 ++- .../tutorials/soap/30-WSDL-Rewriter.yaml | 16 ++++--- .../soap/40-WSDL-Message-Validation.yaml | 12 ++++- .../tutorials/soap/90-Manual-SOAPProxy.yaml | 32 +++++++++++++ 14 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java rename distribution/tutorials/data/{city-request.soap.xml => city.soap.xml} (100%) create mode 100644 distribution/tutorials/soap/90-Manual-SOAPProxy.yaml diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/InterceptorUtil.java b/core/src/main/java/com/predic8/membrane/core/interceptor/InterceptorUtil.java index fc34712a5e..c0bc0a6d6b 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/InterceptorUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/InterceptorUtil.java @@ -14,10 +14,11 @@ package com.predic8.membrane.core.interceptor; import java.util.*; +import java.util.function.*; public class InterceptorUtil { - public static List getInterceptors(List interceptors, Class clazz) { + public static List getInterceptors(List interceptors, Class clazz) { return interceptors.stream().filter(i -> i.getClass().equals(clazz)) .map(clazz::cast) .toList(); @@ -27,5 +28,47 @@ public static Optional getFirstInterceptorOfType(List return getInterceptors(interceptors, type).stream().findFirst(); } + /** + * Ensures that an interceptor of the given {@code type} is in the first position. + *

+ * Behavior: + * - If an interceptor of exactly {@code type} exists, the first occurrence is moved to index 0. + * - If none exists, a new instance is created using {@code supplier}, inserted at index 0, and returned. + * - Returns {@link Optional#empty()} only if {@code supplier} returns {@code null}. + *

+ * Notes: + * - Matches by exact class equality (same as {@link #getInterceptors(List, Class)}). + * - Modifies the provided list in place. + */ + public static Optional moveToFirstPosition( + List interceptors, + Class type, + Supplier supplier + ) { + // Find first exact match + for (int i = 0; i < interceptors.size(); i++) { + Interceptor current = interceptors.get(i); + if (current != null && current.getClass().equals(type)) { + @SuppressWarnings("unchecked") + T typed = (T) current; + + if (i != 0) { + // Remove and re-add at front + interceptors.remove(i); + interceptors.addFirst(typed); + } + return Optional.of(typed); + } + } + + // Not found: create and add + T created = supplier.get(); + if (created == null) { + return Optional.empty(); + } + interceptors.addFirst(created); + return Optional.of(created); + } + } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index 3276144f36..c7d7018829 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -26,13 +26,12 @@ import javax.xml.namespace.*; import java.io.*; import java.net.*; -import java.util.regex.*; import static com.predic8.membrane.core.Constants.*; import static com.predic8.membrane.core.http.Header.*; import static com.predic8.membrane.core.http.Request.*; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.Set.*; -import static com.predic8.membrane.core.util.soap.WSDLUtil.rewriteRelativeWsdlPath; +import static com.predic8.membrane.core.util.soap.WSDLUtil.*; import static java.nio.charset.StandardCharsets.*; /** diff --git a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java index 5ad4f46080..e12e0699f3 100644 --- a/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/proxies/SOAPProxy.java @@ -36,6 +36,8 @@ import java.util.*; import java.util.regex.*; +import static com.predic8.membrane.core.interceptor.InterceptorUtil.moveToFirstPosition; + /** * @description

* A SOAP proxy automatically configures itself using a WSDL description. It reads the WSDL to extract: @@ -240,12 +242,7 @@ public static Port selectPort(List ports, String portName) { } private WSDLInterceptor addAndGetWSDLInterceptor() { - return getFirstInterceptorOfType(WSDLInterceptor.class) - .orElseGet(() -> { - var i = new WSDLInterceptor(); - interceptors.addFirst(i); - return i; - }); + return moveToFirstPosition(interceptors, WSDLInterceptor.class, WSDLInterceptor::new).orElseThrow(); } private void addWebServiceExplorer() { diff --git a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java index f164751065..ef02ca1718 100644 --- a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java +++ b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java @@ -32,15 +32,11 @@ public class ResponseTest { private Response res1; - private Response res2; - private Response res3; private InputStream in1; - private InputStream in2; - private InputStream in3; private ByteArrayOutputStream tempOut; @@ -304,4 +300,31 @@ void readResponseNoBodyContent205() throws IOException, EndOfStreamException { assertTrue(res.isBodyEmpty()); assertInstanceOf(EmptyBody.class, res.getBody()); } + + @Nested + class RealResponses { + + private static final String chunkedCharset = """ + HTTP/1.1 404 Not Found + date: Fri, 09 Jan 2026 13:29:16 GMT + content-type: text/plain; charset=us-ascii + transfer-encoding: chunked + set-cookie: cc370ea6bd994adee940ea801664=09ea03894683df52e5cfd3fa1c8; path=/; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: max-age=157680000; includeSubDomains + + 17 + No Mapping Rule matched + 0 + + """; + + @Test + void chunkedCharset() throws Exception { + Response res = new Response(); + res.read(new ByteArrayInputStream( StringTestUtil.normalizeCRLF(chunkedCharset).getBytes()),true); + assertEquals(404, res.getStatusCode()); + assertEquals("text/plain; charset=us-ascii", res.getHeader().getContentType()); + assertEquals("No Mapping Rule matched",res.getBodyAsStringDecoded()); + } + } } diff --git a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java index 11711d3700..26b856aca2 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java +++ b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java @@ -21,4 +21,19 @@ public class StringTestUtil { public static InputStream inputStreamFrom(String string) { return new ByteArrayInputStream(string.getBytes()); } + + /** + * For tests that need a CRLF terminated HTTP message but the message is provided as a Java String. + * @param s String with HTTP message with LF terminated lines. + * @return String with HTTP message with CRLF terminated lines. + */ + public static String normalizeCRLF(String s) { + // First normalize all possible line endings to LF + String lf = s.replace("\r\n", "\n") + .replace("\r", "\n"); + + // Then convert LF to CRLF + return lf.replace("\n", "\r\n"); + } + } diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java index 0d3bc5b39a..1f170cc595 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java @@ -47,7 +47,7 @@ void wsdl() { void soapCall() throws IOException { given() // File is read from FS use the same file as the user - .body(readFileFromBaseDir("../data/city-request.soap.xml")) + .body(readFileFromBaseDir("../data/city.soap.xml")) .when() .post("http://localhost:2000/city-service") .then() diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java new file mode 100644 index 0000000000..2c17db1047 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java @@ -0,0 +1,47 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +import org.junit.jupiter.api.*; + +import java.io.*; + +import static com.predic8.membrane.core.http.MimeType.*; +import static io.restassured.RestAssured.*; +import static org.hamcrest.Matchers.*; + +public class ManualSOAPProxyTutorialTest extends WSDLRewriterTutorialTest { + + @Override + protected String getTutorialYaml() { + return "90-Manual-SOAPProxy.yaml"; + } + + @Test + void soapCallInvalid() throws IOException { + // @formatter:off + given() + // File is read from FS uses the same file as the user + .body(readFileFromBaseDir("../data/invalid-city.soap.xml")) + .contentType(TEXT_XML_UTF8) + .when() + .post("http://localhost:2000/my-service") + .then() + .log().body() + .body("Envelope.Body.Fault.faultstring", equalTo("WSDL message validation failed")) + .body(containsString("INVALID") ); + // @formatter:on + } +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java index 380ccaec92..23a21c24f8 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java @@ -51,7 +51,7 @@ void soapCall() throws IOException { // @formatter:off given() // File is read from FS use the same file as the user - .body(readFileFromBaseDir("../data/city-request.soap.xml")) + .body(readFileFromBaseDir("../data/city.soap.xml")) .when() .post("http://localhost:2000/my-service") .then() diff --git a/distribution/tutorials/data/city-request.soap.xml b/distribution/tutorials/data/city.soap.xml similarity index 100% rename from distribution/tutorials/data/city-request.soap.xml rename to distribution/tutorials/data/city.soap.xml diff --git a/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml b/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml index 0fccfb5309..cdfdcf9673 100644 --- a/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml +++ b/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml @@ -4,7 +4,7 @@ # # A minimal SOAP endpoint for development and debugging. # -# 1.) Run +# 1.) Start Membrane # Linux/Mac: # ./membrane.sh -c 10-Sample-SOAP-Service.yaml # Windows: @@ -14,7 +14,7 @@ # http://localhost:2000/city-service?wsdl # # 3.) Invoke -# curl -d @../data/city-request.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service +# curl -d @../data/city.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service api: port: 2000 diff --git a/distribution/tutorials/soap/20-SOAPProxy.yaml b/distribution/tutorials/soap/20-SOAPProxy.yaml index 3537adec66..50d407eedc 100644 --- a/distribution/tutorials/soap/20-SOAPProxy.yaml +++ b/distribution/tutorials/soap/20-SOAPProxy.yaml @@ -10,13 +10,17 @@ # The serviceProxy rewrites the WSDL automatically on the fly. # # 1.) Start Membrane +# Linux/Mac: +# ./membrane.sh -c 20-SOAPProxy.yaml +# Windows: +# membrane.cmd -c 20-SOAPProxy.yaml # # 2.) WSDL # http://localhost:2000/city-service?wsdl # Take a look at the address/@location attribute at the end of the WSDL # # 3.) Invoke -# curl -d @../data/city-request.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service +# curl -d @../data/city.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service soapProxy: port: 2000 diff --git a/distribution/tutorials/soap/30-WSDL-Rewriter.yaml b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml index 5cf29166eb..f1dab6f4e6 100644 --- a/distribution/tutorials/soap/30-WSDL-Rewriter.yaml +++ b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml @@ -2,16 +2,17 @@ # # Tutorial: WSDL Rewriter # -# By default, WSDL rewriting uses the hostname and port on which the API Gateway -# is reachable. This is sufficient for direct access. +# If Membrane runs behind a reverse proxy or load balancer, the public URL differs from +# the internal one. In that case, override the external host, protocol, and port so the +# generated WSDL describes the public endpoint. # -# If the API Gateway is deployed behind a reverse proxy or load balancer, these -# defaults are usually incorrect. In such cases, the public-facing host, protocol, -# port, and path must be rewritten explicitly. -# -# This example shows how to override these values. +# This example shows how to override the default values. # # 1.) Start Membrane +# Linux/Mac: +# ./membrane.sh -c 30-WSDL-Rewriter.yaml +# Windows: +# membrane.cmd -c 30-WSDL-Rewriter.yaml # # 2.) WSDL # http://localhost:2000/my-service?wsdl @@ -23,6 +24,7 @@ soapProxy: port: 2000 wsdl: https://www.predic8.de/city-service?wsdl flow: + # You should find those values in the WSDL returned by the proxy above. - wsdlRewriter: host: my.host.example.com protocol: https diff --git a/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml b/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml index ecfe28fd41..38defe0ece 100644 --- a/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml +++ b/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml @@ -2,12 +2,20 @@ # # Tutorial: WSDL Message Validation # +# A WSDL document and its imported or included XML Schemas define +# the exact structure of valid SOAP requests and responses. +# +# The WSDL validator loads the WSDL and all referenced XSDs and +# validates SOAP message against these definitions. +# Invalid messages are rejected before they reach the backend or +# the client. # # 1.) Start Membrane +# ./membrane.sh -c 40-WSDL-Message-Validation.yaml # -# 2.) Send request with invalid XML +# 2.) Send valid and invalid SOAP messages +# curl -d @../data/city.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service # curl -d @../data/invalid-city.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service -# Response should contain a SOAP Fault soapProxy: port: 2000 diff --git a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml new file mode 100644 index 0000000000..a738f8ecc1 --- /dev/null +++ b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# Tutorial: Manual SOAP Proxy +# + +api: + path: + uri: /manual-soap-proxy + flow: + - wsdlRewriter: + host: my.host.example.com + protocol: https + port: 443 + - wsdlPublisher: {} + - wsdlExplorer: {} + - validator: {} + + +--- + +soapProxy: + path: + uri: /my-service + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl + flow: + # You should find those values in the WSDL returned by the proxy above. + - wsdlRewriter: + host: my.host.example.com + protocol: https + port: 443 + - validator: {} From f38ebfc6ed7bbef3d413282caaf39b2e2579cba5 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 11 Jan 2026 17:05:34 +0100 Subject: [PATCH 12/15] feat: enhance WSDL proxy support and improve documentation - Added `path` attribute to `WSDLInterceptor` for configurable WSDL rewrite paths. - Improved error logging and exception handling in `WSDLInterceptor`. - Updated `90-Manual-SOAPProxy.yaml` with refined tutorial structure and enhanced example configuration. - Clarified `normalizeCRLF` utility documentation. - Added new roadmap items for stream tracing improvements. --- .../core/interceptor/WSDLInterceptor.java | 27 ++++++++++++- .../membrane/core/util/StringTestUtil.java | 2 +- .../tutorials/soap/90-Manual-SOAPProxy.yaml | 40 +++++++++++++++---- docs/ROADMAP.md | 4 +- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java index c7d7018829..3135688084 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/WSDLInterceptor.java @@ -51,6 +51,11 @@ public class WSDLInterceptor extends RelocatingInterceptor { private boolean rewriteEndpoint = true; private HttpClient hc; + /** + * Path of the service location to rewrite + */ + private String path; + public WSDLInterceptor() { name = "wsdl rewriting"; setAppliedFlow(RESPONSE_FLOW); @@ -60,6 +65,9 @@ public WSDLInterceptor() { public void init() { super.init(); hc = router.getHttpClientFactory().createClient(null); + + if (path != null) + setPathRewriterOnWSDLInterceptor(path); } public void setPathRewriterOnWSDLInterceptor(String keypath) { @@ -76,9 +84,9 @@ public void setPathRewriterOnWSDLInterceptor(String keypath) { } return rewriteRelativeWsdlPath(path, URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), keypath)); } catch (URISyntaxException | MalformedURLException e) { - log.error("Cannot parse URL {} - {}", path,e.getMessage()); + log.error("Cannot parse URL {} - {}", path, e); + throw new RuntimeException(e); } - return path; }; } @@ -226,4 +234,19 @@ public void setPort(int port) { super.setPort(Integer.toString(port)); } + /** + * When the wsdlRewriter is used in a SOAPProxy, the path is set to the path/uri from the SOAPProxy. + * + * @param path + * @description Path to use for the service location + * @default soapProxy/path/uri + */ + @MCAttribute + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } } diff --git a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java index 26b856aca2..16e5c04143 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java +++ b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java @@ -24,7 +24,7 @@ public static InputStream inputStreamFrom(String string) { /** * For tests that need a CRLF terminated HTTP message but the message is provided as a Java String. - * @param s String with HTTP message with LF terminated lines. + * @param s String with HTTP message. Line ending does not matter. * @return String with HTTP message with CRLF terminated lines. */ public static String normalizeCRLF(String s) { diff --git a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml index a738f8ecc1..982ba1b596 100644 --- a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml +++ b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml @@ -1,27 +1,51 @@ # yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json # -# Tutorial: Manual SOAP Proxy +# Tutorial: Manual SOAPProxy # +# Membrane provides the `soapProxy` shortcut for SOAP web services. +# A `soapProxy` reads the WSDL and automatically adds: +# - `wsdlRewriter` +# - `wsdlPublisher` +# - `webServiceExplorer` +# +# `soapProxy` is convenient, but it does not give you full control over the interceptor chain. +# If you need precise ordering or want to include only specific interceptors (e.g. only a +# rewriter or only a validator), define an `api` and wire the interceptors manually. +# +# The `api` and the `soapProxy` below are equivalent. api: + port: 2000 path: - uri: /manual-soap-proxy + uri: /my-service flow: + # The wsdlRewriter must be the first of the following interceptors. + - rewriter: + - map: + # Put values in quotes when using special characters + from: "/my-service/(.*)" + to: "/city-service/$1" - wsdlRewriter: host: my.host.example.com protocol: https port: 443 - - wsdlPublisher: {} - - wsdlExplorer: {} - - validator: {} - + path: /my-service + - wsdlPublisher: + wsdl: https://www.predic8.de/city-service?wsdl + - webServiceExplorer: + wsdl: https://www.predic8.de/city-service?wsdl + # The validator must be the last + - validator: + wsdl: https://www.predic8.de/city-service?wsdl + target: + url: https://www.predic8.de/city-service --- - +# Same behavior as above, but using a soapProxy soapProxy: path: uri: /my-service - port: 2000 + port: 2001 wsdl: https://www.predic8.de/city-service?wsdl flow: # You should find those values in the WSDL returned by the proxy above. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b899537401..1386840cdb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -24,7 +24,9 @@ - Refine YAML for balancer: clustersFromSpring - wsdlRewriter YAML is not working - use @MCElement(collapsed=true) for suitable classes - +- StreamTracing: + - Take out zeros + - Line Break after [] # 7.0.4 From 1689c03cb7ee04842b5d277934ece2c1fd0c3636 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 11 Jan 2026 17:07:36 +0100 Subject: [PATCH 13/15] refactor(tests): simplify method annotations in `ResponseTest` - Removed unnecessary `@Test` annotations from test methods. - Streamlined method definitions and improved readability. --- .../membrane/core/http/ResponseTest.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java index ef02ca1718..e26196329e 100644 --- a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java +++ b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java @@ -76,28 +76,28 @@ public void tearDown() throws Exception { } @Test - public void testParseStartLine1() throws IOException, EndOfStreamException { + void parseStartLine1() throws IOException { res1.parseStartLine(in1); assertEquals(200, res1.getStatusCode()); assertEquals("1.1", res1.getVersion()); } @Test - public void testParseStartLine2() throws IOException, EndOfStreamException { + void testParseStartLine2() throws IOException { res2.parseStartLine(in2); assertEquals(200, res2.getStatusCode()); assertEquals("1.1", res2.getVersion()); } @Test - public void testParseStartLine3() throws IOException, EndOfStreamException { + void testParseStartLine3() throws IOException { res3.parseStartLine(in3); assertEquals(200, res3.getStatusCode()); assertEquals("1.1", res3.getVersion()); } @Test - public void testUnchunkedHtmlRead() throws Exception { + void testUnchunkedHtmlRead() throws Exception { res1.read(in1, true); assertEquals(200, res1.getStatusCode()); assertTrue(res1.isHTTP11()); @@ -108,12 +108,11 @@ public void testUnchunkedHtmlRead() throws Exception { } @Test - public void testUnchunkedHtmlWrite() throws Exception { + void testUnchunkedHtmlWrite() throws Exception { tempOut = new ByteArrayOutputStream(); res1.read(in1, true); res1.write(tempOut, true); - tempIn = new ByteArrayInputStream(tempOut.toByteArray()); Response resTemp = new Response(); @@ -126,7 +125,7 @@ public void testUnchunkedHtmlWrite() throws Exception { } @Test - public void testUnchunkedImageRead() throws Exception { + void testUnchunkedImageRead() throws Exception { res2.read(in2, true); assertEquals(200, res2.getStatusCode()); assertTrue(res2.isHTTP11()); @@ -138,7 +137,7 @@ public void testUnchunkedImageRead() throws Exception { @Test - public void testUnchunkedImageWrite() throws Exception { + void testUnchunkedImageWrite() throws Exception { tempOut = new ByteArrayOutputStream(); res2.read(in2, true); res2.write(tempOut, true); @@ -156,9 +155,8 @@ public void testUnchunkedImageWrite() throws Exception { assertArrayEquals(res2.getBody().getContent(), resTemp.getBody().getContent()); } - @Test - public void testChunkedHtmlRead() throws Exception { + void testChunkedHtmlRead() throws Exception { res3.read(in3, true); assertEquals(200, res3.getStatusCode()); assertTrue(res3.isHTTP11()); @@ -168,12 +166,11 @@ public void testChunkedHtmlRead() throws Exception { @Test - public void testChunkedHtmlWrite() throws Exception { + void testChunkedHtmlWrite() throws Exception { tempOut = new ByteArrayOutputStream(); res3.read(in3, true); res3.write(tempOut, true); - tempIn = new ByteArrayInputStream(tempOut.toByteArray()); Response resTemp = new Response(); @@ -187,7 +184,6 @@ public void testChunkedHtmlWrite() throws Exception { assertArrayEquals(res3.getBody().getContent(), resTemp.getBody().getContent()); } else assertEquals(res3.getBody().getContent().length, 0); - } @Test @@ -202,7 +198,7 @@ void isEmpty() throws IOException { } @Test - public void isNotEmpty() throws Exception { + void isNotEmpty() throws Exception { assertFalse(ok("ABC").build().isBodyEmpty()); } From 09772ff3548be5818f4b5ec55d45649f4adc9dbc Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 11 Jan 2026 17:24:19 +0100 Subject: [PATCH 14/15] refactor(tests): standardize UTF-8 usage and modularize test structures - Updated `StringTestUtil` to ensure consistent UTF-8 encoding in `inputStreamFrom`. - Refactored `WSDLRewriterTutorialTest` and related test URLs to dynamically use ports via `getPort()`. - Introduced two new test classes (`APIManualSOAPProxyTutorialTest` and `SoapProxyManualSOAPProxyTutorialTest`) for enhanced modularity and clarity. - Converted `ManualSOAPProxyTutorialTest` into an abstract class for better reusability. --- .../membrane/core/util/StringTestUtil.java | 5 +++- .../soap/APIManualSOAPProxyTutorialTest.java | 23 +++++++++++++++++++ ... AbstractManualSOAPProxyTutorialTest.java} | 4 ++-- .../SoapProxyManualSOAPProxyTutorialTest.java | 23 +++++++++++++++++++ .../soap/WSDLRewriterTutorialTest.java | 10 +++++--- 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java rename distribution/src/test/java/com/predic8/membrane/tutorials/soap/{ManualSOAPProxyTutorialTest.java => AbstractManualSOAPProxyTutorialTest.java} (89%) create mode 100644 distribution/src/test/java/com/predic8/membrane/tutorials/soap/SoapProxyManualSOAPProxyTutorialTest.java diff --git a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java index 16e5c04143..35e8c3e9ab 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java +++ b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java @@ -15,11 +15,14 @@ package com.predic8.membrane.core.util; import java.io.*; +import java.nio.charset.*; + +import static java.nio.charset.StandardCharsets.UTF_8; public class StringTestUtil { public static InputStream inputStreamFrom(String string) { - return new ByteArrayInputStream(string.getBytes()); + return new ByteArrayInputStream(string.getBytes(UTF_8)); } /** diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java new file mode 100644 index 0000000000..ad069ecfcd --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java @@ -0,0 +1,23 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +public class APIManualSOAPProxyTutorialTest extends AbstractManualSOAPProxyTutorialTest { + + @Override + protected int getPort() { + return 2000; + } +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractManualSOAPProxyTutorialTest.java similarity index 89% rename from distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java rename to distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractManualSOAPProxyTutorialTest.java index 2c17db1047..593d5169b9 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/ManualSOAPProxyTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractManualSOAPProxyTutorialTest.java @@ -22,7 +22,7 @@ import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; -public class ManualSOAPProxyTutorialTest extends WSDLRewriterTutorialTest { +public abstract class AbstractManualSOAPProxyTutorialTest extends WSDLRewriterTutorialTest { @Override protected String getTutorialYaml() { @@ -37,7 +37,7 @@ void soapCallInvalid() throws IOException { .body(readFileFromBaseDir("../data/invalid-city.soap.xml")) .contentType(TEXT_XML_UTF8) .when() - .post("http://localhost:2000/my-service") + .post("http://localhost:%d/my-service".formatted(getPort())) .then() .log().body() .body("Envelope.Body.Fault.faultstring", equalTo("WSDL message validation failed")) diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SoapProxyManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SoapProxyManualSOAPProxyTutorialTest.java new file mode 100644 index 0000000000..d900b28228 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SoapProxyManualSOAPProxyTutorialTest.java @@ -0,0 +1,23 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.tutorials.soap; + +public class SoapProxyManualSOAPProxyTutorialTest extends AbstractManualSOAPProxyTutorialTest { + + @Override + protected int getPort() { + return 2001; + } +} diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java index 23a21c24f8..a144c5b3fd 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java @@ -30,12 +30,16 @@ protected String getTutorialYaml() { return "30-WSDL-Rewriter.yaml"; } + protected int getPort() { + return 2000; + } + @Test void wsdl() { // @formatter:off given() .when() - .get("http://localhost:2000/my-service?wsdl") + .get("http://localhost:%d/my-service?wsdl".formatted(getPort())) .then() .statusCode(200) .contentType(XML) @@ -53,7 +57,7 @@ void soapCall() throws IOException { // File is read from FS use the same file as the user .body(readFileFromBaseDir("../data/city.soap.xml")) .when() - .post("http://localhost:2000/my-service") + .post("http://localhost:%d/my-service".formatted(getPort())) .then() .body("Envelope.Body.getCityResponse.population", equalTo("34665600")); // @formatter:on @@ -64,7 +68,7 @@ void webServiceExplorer() { // @formatter:off given() .when() - .get("http://localhost:2000/my-service") + .get("http://localhost:%d/my-service".formatted(getPort())) .then() .statusCode(200) .contentType(HTML) From 0c3e7ba62a3380f31c0d24e2613b9a421611940c Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 11 Jan 2026 17:25:54 +0100 Subject: [PATCH 15/15] refactor(tests): simplify and standardize test behavior - Removed redundant `getPort` method from `APIManualSOAPProxyTutorialTest` by using defaults from superclass. - Reordered assertion parameters in `ResponseTest` for consistency. --- .../java/com/predic8/membrane/core/http/ResponseTest.java | 2 +- .../tutorials/soap/APIManualSOAPProxyTutorialTest.java | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java index e26196329e..09c34dad89 100644 --- a/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java +++ b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java @@ -183,7 +183,7 @@ void testChunkedHtmlWrite() throws Exception { assertEquals(res3.getBody().getContent().length, resTemp.getBody().getContent().length); assertArrayEquals(res3.getBody().getContent(), resTemp.getBody().getContent()); } else - assertEquals(res3.getBody().getContent().length, 0); + assertEquals(0, res3.getBody().getContent().length); } @Test diff --git a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java index ad069ecfcd..b902911249 100644 --- a/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java @@ -14,10 +14,8 @@ package com.predic8.membrane.tutorials.soap; +/** + * With defaults from super the tests will run on port 2000 + */ public class APIManualSOAPProxyTutorialTest extends AbstractManualSOAPProxyTutorialTest { - - @Override - protected int getPort() { - return 2000; - } }