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/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/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/RelocatingInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/RelocatingInterceptor.java index b33d826a3c..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 @@ -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; @@ -74,14 +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; - } - - 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 67ecb950c7..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 @@ -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.*; @@ -30,13 +31,14 @@ 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.*; import static java.nio.charset.StandardCharsets.*; /** * @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") public class WSDLInterceptor extends RelocatingInterceptor { private final static Logger log = LoggerFactory.getLogger(WSDLInterceptor.class.getName()); @@ -49,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); @@ -58,6 +65,29 @@ public WSDLInterceptor() { public void init() { super.init(); hc = router.getHttpClientFactory().createClient(null); + + if (path != null) + setPathRewriterOnWSDLInterceptor(path); + } + + 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, e); + throw new RuntimeException(e); + } + }; } @Override @@ -200,8 +230,23 @@ 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)); + } + + /** + * 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/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..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 @@ -30,7 +30,7 @@ import static com.predic8.membrane.core.http.MimeType.*; import static com.predic8.membrane.core.http.Response.*; 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

@@ -47,6 +47,8 @@ public class WSDLPublisherInterceptor extends AbstractInterceptor { private SOAPProxy soapProxy; + private String wsdl; + public WSDLPublisherInterceptor() { name = "wsdl publisher"; } @@ -54,9 +56,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 { @@ -85,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); } @@ -93,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<>(); @@ -109,7 +124,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); @@ -122,8 +137,6 @@ private void processDocuments(Exchange exc) { } } - private String wsdl; - public String getWsdl() { return wsdl; } @@ -143,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 "); @@ -170,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); @@ -218,7 +218,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 +227,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..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 @@ -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.*; @@ -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.interceptor.InterceptorUtil.moveToFirstPosition; /** * @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; @@ -87,8 +86,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); @@ -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 WebServiceExplorer + var wsdlInterceptor = addAndGetWSDLInterceptor(); // WSDLInterceptor will be first + wsdlInterceptor.setPathRewriterOnWSDLInterceptor(key.getPath()); } private Definitions parseWSDLOnly() { @@ -239,84 +238,15 @@ 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; - } - - 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 void addWSDLInterceptor() { - if (getFirstInterceptorOfType(WSDLInterceptor.class).isEmpty()) { - WSDLInterceptor wsdlInterceptor = new WSDLInterceptor(); - interceptors.addFirst(wsdlInterceptor); - } + return WSDLUtil.getPort(ports); } - private void renameMe() { - 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(); - final String name = getReplacementName(keyPath); - wsdlInterceptor.get().setPathRewriter(path2 -> { - try { - if (path2.contains("://")) { - return new URL(new URL(path2), keyPath).toString(); - } else { - Matcher m = relativePathPattern.matcher(path2); - return m.replaceAll("./" + name + "?"); - } - } catch (MalformedURLException e) { - log.error("Cannot parse URL {}", path2); - } - return path2; - }); - } - - private @NotNull String getReplacementName(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 WSDLInterceptor addAndGetWSDLInterceptor() { + return moveToFirstPosition(interceptors, WSDLInterceptor.class, WSDLInterceptor::new).orElseThrow(); } private void addWebServiceExplorer() { - WebServiceExplorerInterceptor sui = new WebServiceExplorerInterceptor(); + var sui = new WebServiceExplorerInterceptor(); sui.setWsdl(wsdl); sui.setPortName(portName); interceptors.addFirst(sui); @@ -326,7 +256,7 @@ private void addWSDLPublisherInterceptor() { if (hasWSDLPublisherInterceptor()) return; - WSDLPublisherInterceptor wp = new WSDLPublisherInterceptor(); + var wp = new WSDLPublisherInterceptor(); wp.setWsdl(wsdl); wp.init(router); interceptors.addFirst(wp); 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..32df9da936 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/soap/WSDLUtil.java @@ -0,0 +1,62 @@ +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.*; +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("^\\./[^/?]*\\?"); + + public static String rewriteRelativeWsdlPath(String path, String replacementName) { + return relativePathPattern + .matcher(path) + .replaceAll(quoteReplacement("./%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/http/ResponseTest.java b/core/src/test/java/com/predic8/membrane/core/http/ResponseTest.java index f164751065..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 @@ -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; @@ -80,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()); @@ -112,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(); @@ -130,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()); @@ -142,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); @@ -160,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()); @@ -172,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(); @@ -190,8 +183,7 @@ public 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 @@ -206,7 +198,7 @@ void isEmpty() throws IOException { } @Test - public void isNotEmpty() throws Exception { + void isNotEmpty() throws Exception { assertFalse(ok("ABC").build().isBodyEmpty()); } @@ -304,4 +296,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/interceptor/WSDLInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/WSDLInterceptorTest.java index bac66e77b9..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.*; @@ -37,7 +38,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() @@ -48,6 +49,7 @@ public void setUp() throws Exception { exc.setOriginalHostHeader("thomas-bayer.com:80"); interceptor = new WSDLInterceptor(); + interceptor.init(new DummyTestRouter()); } /** @@ -58,19 +60,17 @@ public void setUp() throws Exception { * */ @Test - void testProtocolSet() 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://")); } @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.*")); @@ -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 74a777c369..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.*; @@ -93,12 +94,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 +108,9 @@ 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(); }); } } \ 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/StringTestUtil.java b/core/src/test/java/com/predic8/membrane/core/util/StringTestUtil.java index 11711d3700..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,10 +15,28 @@ 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)); } + + /** + * For tests that need a CRLF terminated HTTP message but the message is provided as a Java String. + * @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) { + // 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/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..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,18 +22,19 @@ 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 { @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 @@ -70,4 +71,15 @@ void getPortFromURLTest() throws MalformedURLException { assertEquals(80, getPortFromURL(new URL("http://localhost"))); assertEquals(443, getPortFromURL(new URL("https://api.predic8.de"))); } + + @Test + 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/")); + } } 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/APIManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java new file mode 100644 index 0000000000..b902911249 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/APIManualSOAPProxyTutorialTest.java @@ -0,0 +1,21 @@ +/* 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; + +/** + * With defaults from super the tests will run on port 2000 + */ +public class APIManualSOAPProxyTutorialTest extends AbstractManualSOAPProxyTutorialTest { +} 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..1f170cc595 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractCityServiceTest.java @@ -0,0 +1,58 @@ +/* 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.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.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/AbstractManualSOAPProxyTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractManualSOAPProxyTutorialTest.java new file mode 100644 index 0000000000..593d5169b9 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/AbstractManualSOAPProxyTutorialTest.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 abstract class AbstractManualSOAPProxyTutorialTest 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:%d/my-service".formatted(getPort())) + .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/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..91336055ad --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SOAPProxyTutorialTest.java @@ -0,0 +1,44 @@ +/* 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.*; +import static io.restassured.http.ContentType.*; +import static org.hamcrest.Matchers.*; + +public class SOAPProxyTutorialTest extends AbstractCityServiceTest { + + @Override + protected String getTutorialYaml() { + return "20-SOAPProxy.yaml"; + } + + @Test + void webServiceExplorer() { + // @formatter:off + given() + .body("Invalid") + .when() + .get("http://localhost:2000/city-service") + .then() + .log().body() + .statusCode(200) + .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..3b1b803f04 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/SampleSOAPServiceTutorialTest.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 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/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/WSDLMessageValidationTutorialTest.java b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java new file mode 100644 index 0000000000..949e2f3f05 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLMessageValidationTutorialTest.java @@ -0,0 +1,48 @@ +/* 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 WSDLMessageValidationTutorialTest extends AbstractCityServiceTest { + + @Override + protected String getTutorialYaml() { + return "40-WSDL-Message-Validation.yaml"; + } + + @Override + @Test + void soapCall() 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/city-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 new file mode 100644 index 0000000000..a144c5b3fd --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/tutorials/soap/WSDLRewriterTutorialTest.java @@ -0,0 +1,78 @@ +/* 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"; + } + + protected int getPort() { + return 2000; + } + + @Test + void wsdl() { + // @formatter:off + given() + .when() + .get("http://localhost:%d/my-service?wsdl".formatted(getPort())) + .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.soap.xml")) + .when() + .post("http://localhost:%d/my-service".formatted(getPort())) + .then() + .body("Envelope.Body.getCityResponse.population", equalTo("34665600")); + // @formatter:on + } + + @Test + void webServiceExplorer() { + // @formatter:off + given() + .when() + .get("http://localhost:%d/my-service".formatted(getPort())) + .then() + .statusCode(200) + .contentType(HTML) + .body(containsString("Membrane API Gateway: CityService")); + // @formatter:on + } +} diff --git a/distribution/tutorials/data/city.soap.xml b/distribution/tutorials/data/city.soap.xml new file mode 100644 index 0000000000..9b30e1394e --- /dev/null +++ b/distribution/tutorials/data/city.soap.xml @@ -0,0 +1,7 @@ + + + + Delhi + + + \ No newline at end of file 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/10-Sample-SOAP-Service.yaml b/distribution/tutorials/soap/10-Sample-SOAP-Service.yaml index 08587e2c70..cdfdcf9673 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.) Start Membrane # 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.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 new file mode 100644 index 0000000000..50d407eedc --- /dev/null +++ b/distribution/tutorials/soap/20-SOAPProxy.yaml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# 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 +# 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.soap.xml -H "Content-Type: text/xml" http://localhost:2000/city-service + +soapProxy: + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl \ No newline at end of file diff --git a/distribution/tutorials/soap/30-WSDL-Rewriter.yaml b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml new file mode 100644 index 0000000000..f1dab6f4e6 --- /dev/null +++ b/distribution/tutorials/soap/30-WSDL-Rewriter.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# Tutorial: WSDL Rewriter +# +# 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. +# +# 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 +# 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: + # You should find those values in the WSDL returned by the proxy above. + - wsdlRewriter: + host: my.host.example.com + protocol: https + port: 443 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..38defe0ece --- /dev/null +++ b/distribution/tutorials/soap/40-WSDL-Message-Validation.yaml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# 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 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 + +soapProxy: + port: 2000 + wsdl: https://www.predic8.de/city-service?wsdl + flow: + - validator: {} diff --git a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml new file mode 100644 index 0000000000..982ba1b596 --- /dev/null +++ b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.0.json +# +# 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: /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 + 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: 2001 + 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: {} 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 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