diff --git a/annot/src/main/java/com/predic8/membrane/annot/Constants.java b/annot/src/main/java/com/predic8/membrane/annot/Constants.java index bebbe4ba45..50c0d9b42c 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/Constants.java +++ b/annot/src/main/java/com/predic8/membrane/annot/Constants.java @@ -14,6 +14,7 @@ package com.predic8.membrane.annot; +import javax.xml.*; import javax.xml.namespace.*; import java.io.*; import java.util.*; @@ -61,6 +62,10 @@ public class Constants { public static final String HTTP_VERSION_11 = "1.1"; + public static final String XMLNS_NS = "http://www.w3.org/2000/xmlns/"; + + public static final String WSDL11_NS = "http://schemas.xmlsoap.org/wsdl/"; + public static final String WSDL_SOAP11_NS = "http://schemas.xmlsoap.org/wsdl/soap/"; public static final String WSDL_SOAP12_NS = "http://schemas.xmlsoap.org/wsdl/soap12/"; public static final String WSDL_HTTP_NS = "http://schemas.xmlsoap.org/wsdl/http/"; @@ -85,7 +90,7 @@ public enum SoapVersion { SOAP11, SOAP12, SOAP20, UNKNOWN } /** - * Used for {@link Request}-to-XML and XML-to-{@link Response} conversions. + * Used for Request-to-XML and XML-to-Response conversions. * See {@link REST2SOAPInterceptor}. */ public static final String HTTP_NS = "http://membrane-soa.org/schemas/http/v1/"; diff --git a/core/pom.xml b/core/pom.xml index 9c2884792e..3cecc647ce 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,9 @@ 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. ---> +--> + 4.0.0 service-proxy-core @@ -89,20 +91,6 @@ com.googlecode.jatl jatl - - com.predic8 - soa-model-core - - - org.apache.httpcomponents - httpclient - - - org.slf4j - jcl-over-slf4j - - - commons-fileupload commons-fileupload @@ -230,6 +218,10 @@ org.apache.groovy groovy-templates + + org.apache.groovy + groovy-json + @@ -349,7 +341,7 @@ 5.19.1 test - + io.rest-assured rest-assured test @@ -486,7 +478,8 @@ - + 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 80ff036c76..ebdcd19357 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 @@ -82,7 +82,7 @@ protected String getLocationHost(Exchange exc) { if (host != null) return host; - String locHost = exc.getOriginalHostHeaderHost(); + var locHost = exc.getOriginalHostHeaderHost(); log.debug("host {}",locHost); 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 f53e9232e1..01465e3f38 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 @@ -16,8 +16,6 @@ import com.predic8.membrane.annot.*; 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.*; @@ -28,11 +26,8 @@ import java.net.*; import static com.predic8.membrane.annot.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.*; -import static java.nio.charset.StandardCharsets.*; /** * @description

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

@@ -47,9 +42,7 @@ public class WSDLInterceptor extends RelocatingInterceptor { public static final QName XSD_INCLUDE_QNAME = new QName(XSD_NS, "include"); public static final String LOCATION = "location"; - private String registryWSDLRegisterURL; private boolean rewriteEndpoint = true; - private HttpClient hc; /** * Path of the service location to rewrite @@ -64,7 +57,6 @@ public WSDLInterceptor() { @Override public void init() { super.init(); - hc = router.getHttpClientFactory().createClient(null); if (path != null) setPathRewriterOnWSDLInterceptor(path); @@ -94,12 +86,9 @@ public void setPathRewriterOnWSDLInterceptor(String keypath) { protected void rewrite(Exchange exc) throws Exception { log.debug("Changing endpoint address in WSDL"); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - Relocator relocator = getRelocator(exc, stream); + var stream = new ByteArrayOutputStream(); + var relocator = getRelocator(exc, stream); relocator.relocate(exc.getResponse().getBodyAsStream()); - if (relocator.isWsdlFound()) { - registerWSDL(exc); - } exc.getResponse().setBodyContent(stream.toByteArray()); // Preserve existing Content-Type if present; otherwise set a sane default including charset. @@ -109,7 +98,7 @@ protected void rewrite(Exchange exc) throws Exception { } private @NotNull Relocator getRelocator(Exchange exc, OutputStream stream) throws Exception { - Relocator relocator = createRelocator(exc, stream); + var relocator = createRelocator(exc, stream); if (rewriteEndpoint) { relocator.getRelocatingAttributes().put(WSDL11_ADDRESS_SOAP11, LOCATION); @@ -128,71 +117,6 @@ protected void rewrite(Exchange exc) throws Exception { getLocationPort(exc), exc.getHandler().getContextPath(exc), pathRewriter); } - private void registerWSDL(Exchange exc) { - if (registryWSDLRegisterURL == null) - return; - - StringBuilder buf = new StringBuilder(2000); - buf.append(registryWSDLRegisterURL); - buf.append("?wsdl="); - - buf.append(URLDecoder.decode(getWSDLURL(exc), US_ASCII)); - - callRegistry(buf.toString()); - - log.debug(buf.toString()); - } - - private void callRegistry(String uri) { - try { - var exchange = createExchange(uri); - hc.call(exchange); - Response res = exchange.getResponse(); - if (res.getStatusCode() != 200) - log.warn("{}", res); - } catch (Exception e) { - log.error("", e); - } - } - - private Exchange createExchange(String uri) throws MalformedURLException, URISyntaxException { - return get(getCompletePath(new URL(uri))).header(HOST, getHost(uri)).buildExchange(); - - } - - private static String getHost(String uri) throws MalformedURLException { - return new URL(uri).getHost(); - } - - private String getCompletePath(URL url) { - if (url.getQuery() == null) - return url.getPath(); - return url.getPath() + "?" + url.getQuery(); - } - - private String getWSDLURL(Exchange exc) { - StringBuilder buf = new StringBuilder(); - buf.append(getLocationProtocol()); - buf.append("://"); - buf.append(getLocationHost(exc)); - if (getLocationPort(exc) != 80) { - buf.append(":"); - buf.append(getLocationPort(exc)); - } - buf.append("/"); - buf.append(exc.getRequest().getUri()); - return buf.toString(); - } - - @MCAttribute - public void setRegistryWSDLRegisterURL(String registryWSDLRegisterURL) { - this.registryWSDLRegisterURL = registryWSDLRegisterURL; - } - - public String getRegistryWSDLRegisterURL() { - return registryWSDLRegisterURL; - } - @Override public String getShortDescription() { return "Rewrites SOAP endpoint addresses and XML Schema locations in WSDL and XSD documents."; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java index 3dd0fbe998..cb53c61be8 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java @@ -318,7 +318,7 @@ private String createClientToken(FlowContext flowContext) { } public InputStream resolve(ResolverMap rm, String baseLocation, String url) throws Exception { - url = ResolverMap.combine(baseLocation, url); + url = ResolverMap.combine( baseLocation, url); // ask the internal httpClient (might be proxied/authenticated), if HTTP if (url.startsWith("http")) { var exc = get(url).buildExchange(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/AbstractXMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/AbstractXMLSchemaValidator.java index 2667ec39ef..7c4ebd77c8 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/AbstractXMLSchemaValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/AbstractXMLSchemaValidator.java @@ -20,13 +20,13 @@ import com.predic8.membrane.core.multipart.*; import com.predic8.membrane.core.resolver.*; import com.predic8.membrane.core.util.*; -import com.predic8.schema.Schema; import org.jetbrains.annotations.*; import org.slf4j.*; +import org.w3c.dom.*; import org.xml.sax.*; import javax.xml.transform.*; -import javax.xml.transform.stream.*; +import javax.xml.transform.dom.*; import javax.xml.validation.*; import java.io.*; import java.util.*; @@ -39,155 +39,141 @@ public abstract class AbstractXMLSchemaValidator extends AbstractMessageValidator { - private static final Logger log = LoggerFactory.getLogger(AbstractXMLSchemaValidator.class.getName()); - - private ArrayBlockingQueue> validators; - protected final XOPReconstitutor xopr; - protected final String location; - protected final ResolverMap resolver; - protected final ValidatorInterceptor.FailureHandler failureHandler; - private final boolean skipFaults; - - protected final AtomicLong valid = new AtomicLong(); - protected final AtomicLong invalid = new AtomicLong(); - - public AbstractXMLSchemaValidator(ResolverMap resolver, String location, ValidatorInterceptor.FailureHandler failureHandler) { - this(resolver, location, failureHandler, false); - } - - public AbstractXMLSchemaValidator(ResolverMap resolver, String location, ValidatorInterceptor.FailureHandler failureHandler, boolean skipFaults) { - this.location = location; - this.resolver = resolver; - this.failureHandler = failureHandler; - this.skipFaults = skipFaults; - xopr = new XOPReconstitutor(); - } - - public void init() { - super.init(); - int concurrency = Runtime.getRuntime().availableProcessors() * 2; - validators = new ArrayBlockingQueue<>(concurrency); - for (int i = 0; i < concurrency; i++) - validators.add(createValidators()); - } - - public Outcome validateMessage(Exchange exc, Interceptor.Flow flow) throws Exception { - Message msg = exc.getMessage(flow); - List exceptions = new ArrayList<>(); - String preliminaryError = getPreliminaryError(xopr, msg); - if (preliminaryError == null) { - List vals = validators.take(); - try { - // the message must be valid for one schema embedded into WSDL - for (Validator validator: vals) { - SchemaValidatorErrorHandler handler = (SchemaValidatorErrorHandler)validator.getErrorHandler(); - try { + private static final Logger log = LoggerFactory.getLogger(AbstractXMLSchemaValidator.class.getName()); + protected final XOPReconstitutor xopr; + protected final String location; + protected final ResolverMap resolver; + protected final ValidatorInterceptor.FailureHandler failureHandler; + protected final AtomicLong valid = new AtomicLong(); + protected final AtomicLong invalid = new AtomicLong(); + private ArrayBlockingQueue> validators; + + public AbstractXMLSchemaValidator(ResolverMap resolver, String location, ValidatorInterceptor.FailureHandler failureHandler) { + this.location = location; + this.resolver = resolver; + this.failureHandler = failureHandler; + xopr = new XOPReconstitutor(); + } + + private static @NotNull Map createErrorEntry(Exception e) { + var error = new LinkedHashMap(); + error.put("message", e.getMessage()); + if (e instanceof SAXParseException spe) { + error.put("line", spe.getLineNumber()); + error.put("column", spe.getColumnNumber()); + } + return error; + } + + public void init() { + super.init(); + int concurrency = Runtime.getRuntime().availableProcessors() * 2; + validators = new ArrayBlockingQueue<>(concurrency); + for (int i = 0; i < concurrency; i++) + validators.add(createValidators()); + } + + public Outcome validateMessage(Exchange exc, Interceptor.Flow flow) throws Exception { + var msg = exc.getMessage(flow); + var exceptions = new ArrayList(); + var preliminaryError = getPreliminaryError(xopr, msg); + if (preliminaryError == null) { + List vals = validators.take(); + try { + // the message must be valid for one schema embedded into WSDL + for (var validator : vals) { + var handler = (SchemaValidatorErrorHandler) validator.getErrorHandler(); + try { validator.validate(getMessageBody(xopr.reconstituteIfNecessary(msg))); - if (handler.noErrors()) { - valid.incrementAndGet(); - return CONTINUE; - } - exceptions.add(handler.getException()); - } finally { - handler.reset(); - } - } - } catch (Exception e) { - exceptions.add(e); - } finally { - validators.put(vals); - } - } else { - exceptions.add(new Exception(preliminaryError)); - } - if (skipFaults && isFault(msg)) { - valid.incrementAndGet(); - return CONTINUE; - } - String errorMsg = getErrorMsg(exceptions); // Errors als simple String - if (failureHandler != null) { - failureHandler.handleFailure(errorMsg, exc); - } - exc.setProperty("error", errorMsg); // TODO Search for usage. If it is used rename property. See properties in class Exchange - setErrorResponse(exc,flow,exceptions); - exc.getResponse().getHeader().add(VALIDATION_ERROR_SOURCE, flow.name()); - invalid.incrementAndGet(); - return ABORT; - } - - protected List createValidators() { - SchemaFactory sf = SchemaFactory.newInstance(XSD_NS); - sf.setResourceResolver(resolver.toLSResourceResolver()); - List validators = new ArrayList<>(); - for (Schema schema : getSchemas()) { - log.debug("Creating validator for schema: {}", schema); - validators.add(getValidator(schema, sf)); - } - return validators; - } - - private @NotNull Validator getValidator(Schema schema, SchemaFactory sf) { + if (handler.noErrors()) { + valid.incrementAndGet(); + return CONTINUE; + } + exceptions.add(handler.getException()); + } finally { + handler.reset(); + } + } + } catch (Exception e) { + exceptions.add(e); + } finally { + validators.put(vals); + } + } else { + exceptions.add(new Exception(preliminaryError)); + } + var errorMsg = getErrorMsg(exceptions); // Errors als simple String + if (failureHandler != null) { + failureHandler.handleFailure(errorMsg, exc); + } + exc.setProperty("error", errorMsg); // TODO Search for usage. If it is used rename property. See properties in class Exchange + setErrorResponse(exc, flow, exceptions); + exc.getResponse().getHeader().add(VALIDATION_ERROR_SOURCE, flow.name()); + invalid.incrementAndGet(); + return ABORT; + } + + protected List createValidators() { + var sf = SchemaFactory.newInstance(XSD_NS); + sf.setResourceResolver(resolver.toLSResourceResolver()); + var validators = new ArrayList(); + var schemas = getSchemas(); + for (int i = 0; i < schemas.size(); i++) { + var schema = schemas.get(i); + log.debug("Creating validator {}/{} for schema at: {}", i + 1, schemas.size(), location); + validators.add(createValidator(schema, sf)); + } + return validators; + } + + private @NotNull Validator createValidator(Element schema, SchemaFactory sf) { try { - Validator validator = sf.newSchema(getStreamSource(schema)).newValidator(); - validator.setResourceResolver(resolver.toLSResourceResolver()); - validator.setErrorHandler(new SchemaValidatorErrorHandler()); - return validator; + var source = new DOMSource(schema); + source.setSystemId(location); + var validator = sf.newSchema(source).newValidator(); + validator.setErrorHandler(new SchemaValidatorErrorHandler()); + return validator; } catch (SAXException e) { - throw new ConfigurationException("Cannot read schema %s.".formatted(schema.getName()),e); + throw new ConfigurationException("Cannot read schema %s.".formatted(location), e); + } + } + + private String getErrorMsg(List excs) { + var buf = new StringBuilder(); + buf.append("%s: ".formatted(getErrorTitle())); + for (var e : excs) { + buf.append(e); + buf.append("; "); } - } - - private @NotNull StreamSource getStreamSource(Schema schema) { - StreamSource ss = new StreamSource(new StringReader(schema.getAsString())); - ss.setSystemId(location); - return ss; - } - - private String getErrorMsg(List excs) { - StringBuilder buf = new StringBuilder(); - buf.append("%s: ".formatted(getErrorTitle())); - for (Exception e : excs) { - buf.append(e); - buf.append("; "); - } - return buf.toString(); - } - - protected List> convertExceptionsToMap(List exceptions) { - return exceptions.stream().map(AbstractXMLSchemaValidator::createErrorEntry).toList(); - } - - private static @NotNull Map createErrorEntry(Exception e) { - var error = new LinkedHashMap(); - error.put("message", e.getMessage()); - if (e instanceof SAXParseException spe) { - error.put("line", spe.getLineNumber()); - error.put("column", spe.getColumnNumber()); - } - return error; - } - - @Override - public long getValid() { - return valid.get(); - } - - @Override - public long getInvalid() { - return invalid.get(); - } - - protected abstract List getSchemas(); - protected abstract Source getMessageBody(InputStream input); - - protected abstract void setErrorResponse(Exchange exchange, String message); - protected abstract void setErrorResponse(Exchange exchange, Interceptor.Flow flow,List exceptions); - - protected abstract boolean isFault(Message msg); - protected abstract String getPreliminaryError(XOPReconstitutor xopr, Message msg); - - @Override - public String getErrorTitle() { - return "XML message validation failed"; - } + return buf.toString(); + } + + protected List> convertExceptionsToMap(List exceptions) { + return exceptions.stream().map(AbstractXMLSchemaValidator::createErrorEntry).toList(); + } + + @Override + public long getValid() { + return valid.get(); + } + + @Override + public long getInvalid() { + return invalid.get(); + } + + protected abstract List getSchemas(); + + protected abstract Source getMessageBody(InputStream input); + + protected abstract void setErrorResponse(Exchange exchange, String message); + + protected abstract void setErrorResponse(Exchange exchange, Interceptor.Flow flow, List exceptions); + + protected abstract String getPreliminaryError(XOPReconstitutor xopr, Message msg); + + @Override + public String getErrorTitle() { + return "XML message validation failed"; + } } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java index 7e9b5290cb..65171a350f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java @@ -16,7 +16,7 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.exchange.*; -import com.predic8.membrane.core.http.ReadingBodyException; +import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.schemavalidation.json.*; import com.predic8.membrane.core.proxies.*; @@ -27,14 +27,12 @@ import org.springframework.beans.*; import org.springframework.context.*; -import java.io.*; - import static com.predic8.membrane.core.exceptions.ProblemDetails.*; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; -import static com.predic8.membrane.core.interceptor.Outcome.ABORT; import static com.predic8.membrane.core.interceptor.Outcome.*; -import static com.predic8.membrane.core.resolver.ResolverMap.*; -import static com.predic8.membrane.core.util.text.TextUtil.linkURL; +import static com.predic8.membrane.core.interceptor.Outcome.ABORT; +import static com.predic8.membrane.core.resolver.ResolverMap.combine; +import static com.predic8.membrane.core.util.text.TextUtil.*; /** * Basically switches over {@link WSDLValidator}, {@link XMLSchemaValidator}, @@ -46,7 +44,7 @@ @MCElement(name = "validator") public class ValidatorInterceptor extends AbstractInterceptor implements ApplicationContextAware { - private static final Logger log = LoggerFactory.getLogger(ValidatorInterceptor.class.getName()); + private static final Logger log = LoggerFactory.getLogger(ValidatorInterceptor.class); private String wsdl; private String schema; @@ -99,25 +97,25 @@ private MessageValidator getMessageValidator() throws Exception { if (wsdl != null) { if (schemaMappings != null) logIgnoringRefSchemas(); - return new WSDLValidator(resourceResolver, combine(getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults); + return new WSDLValidator(resourceResolver, combine(router.getConfiguration().getUriFactory(), getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults); } if (schema != null) { if (schemaMappings != null) logIgnoringRefSchemas(); - return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler()); + return new XMLSchemaValidator(resourceResolver, combine(router.getConfiguration().getUriFactory(), getBaseLocation(), schema), createFailureHandler()); } if (jsonSchema != null) { - return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion) {{ + return new JSONYAMLSchemaValidator(resourceResolver, combine(router.getConfiguration().getUriFactory(), getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion) {{ if(schemaMappings != null) setSchemaMappings(schemaMappings.getSchemaMap()); }}; } if (schematron != null) { if (schemaMappings != null) logIgnoringRefSchemas(); - return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext); + return new SchematronValidator(combine(router.getConfiguration().getUriFactory(), getBaseLocation(), schematron), createFailureHandler(), router, applicationContext); } - WSDLValidator validator = getWsdlValidatorFromSOAPProxy(); + var validator = getWsdlValidatorFromSOAPProxy(); if (validator != null) return validator; throw new RuntimeException("Validator is not configured properly. must have an attribute specifying the validator."); @@ -131,7 +129,7 @@ private static void logIgnoringRefSchemas() { if(soapProxy == null) return null; wsdl = soapProxy.getWsdl(); name = "soap validator"; - return new WSDLValidator(resourceResolver, combine(getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults); + return new WSDLValidator(resourceResolver, combine(router.getConfiguration().getUriFactory(), getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults); } private @Nullable String getBaseLocation() { diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractor.java new file mode 100644 index 0000000000..5eab122873 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractor.java @@ -0,0 +1,117 @@ +/* Copyright 2026 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.core.interceptor.schemavalidation; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import com.predic8.membrane.core.util.wsdl.parser.Operation.*; +import org.jetbrains.annotations.*; + +import javax.xml.namespace.*; +import java.util.*; +import java.util.stream.*; + +import static com.predic8.membrane.core.util.wsdl.parser.Binding.Style.*; +import static com.predic8.membrane.core.util.wsdl.parser.Operation.Direction.*; +import static java.util.stream.Collectors.*; + +public class WSDLMessageElementExtractor { + + public static Set getPossibleRequestElements(Definitions definitions, String serviceName) { + return getPossibleElements(definitions, INPUT, serviceName); + } + + public static Set getPossibleResponseElements(Definitions definitions, String serviceName) { + return getPossibleElements(definitions, OUTPUT, serviceName); + } + + public static Set getPossibleElements(Definitions definitions, Direction direction, String serviceName) { + var portTypes = getTypesByStyle(definitions, serviceName); + var result = new HashSet<>(getElementQNameForDocumentStyle(direction, portTypes)); + result.addAll(getElementQNamesForRPCStyle(definitions, direction, portTypes)); + return result; + } + + private static @NotNull Set getElementQNamesForRPCStyle(Definitions definitions, Direction direction, PortTypesByStyle portTypes) { + return portTypes.portTypesRPC().stream().map(PortType::getOperations) + .flatMap(Collection::stream) + .filter(op -> !op.getMessagesByDirection(direction).isEmpty()) + .map(op -> new QName(definitions.getTargetNamespace(), getElementNameRPC(op, direction))) + .collect(toSet()); + } + + private static @NotNull Set getElementQNameForDocumentStyle(Direction direction, PortTypesByStyle portTypes) { + return getParts(direction, portTypes.portTypesDocument()) + .map(Part::getElementQName) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private static @NotNull PortTypesByStyle getTypesByStyle(Definitions definitions, String serviceName) { + if (definitions.getServices().isEmpty()) { + return new PortTypesByStyle(Collections.emptyList(), definitions.getPortTypes()); + } + return getPortTypesByStyle(definitions, serviceName); + } + + private static @NotNull PortTypesByStyle getPortTypesByStyle(Definitions definitions, String serviceName) { + var portTypesRPC = new ArrayList(); + var portTypesDocument = new ArrayList(); + + for (var binding : getBindings(definitions, serviceName)) { + if (binding.getStyle() == RPC) { + portTypesRPC.add(binding.getPortType()); + continue; + } + portTypesDocument.add(binding.getPortType()); + } + return new PortTypesByStyle(portTypesRPC, portTypesDocument); + } + + private static @NotNull List getBindings(Definitions definitions, String serviceName) { + return getServices(definitions, serviceName).stream() + .flatMap(s -> s.getPorts().stream()) + .map(Port::getBinding).toList(); + } + + private static @NotNull List getServices(Definitions definitions, String serviceName) { + if (serviceName != null) { + var service = definitions.getService(serviceName); + if (service.isEmpty()) { + throw new IllegalArgumentException("WSDL does not contain service: " + serviceName); + } + return List.of(service.get()); + } + return definitions.getServices(); + } + + private static String getElementNameRPC(Operation operation, Direction direction) { + if (direction == OUTPUT) { + return operation.getName() + "Response"; + } + return operation.getName(); + } + + private static @NotNull Stream getParts(Direction direction, List result) { + return result.stream().map(PortType::getOperations) + .flatMap(Collection::stream) + .map(op -> op.getMessagesByDirection(direction)) + .flatMap(Collection::stream) + .map(Message::getPart); + } + + private record PortTypesByStyle(List portTypesRPC, List portTypesDocument) { + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractor.java new file mode 100644 index 0000000000..029532f59f --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractor.java @@ -0,0 +1,91 @@ +/* Copyright 2026 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.core.interceptor.schemavalidation; + +import com.predic8.membrane.annot.*; +import org.w3c.dom.*; +import org.xml.sax.*; + +import javax.xml.*; +import javax.xml.parsers.*; +import javax.xml.transform.*; +import javax.xml.transform.dom.*; +import java.io.*; +import java.util.*; + +import static com.predic8.membrane.annot.Constants.XMLNS_NS; +import static com.predic8.membrane.annot.Constants.XSD_NS; +import static javax.xml.XMLConstants.*; + +public final class WSDLSchemaExtractor { + + private WSDLSchemaExtractor() { + } + + public static List getSchemas(Element wsdl) { + try { + var result = new ArrayList(); + var schemas = wsdl.getElementsByTagNameNS(XSD_NS, "schema"); + for (int i = 0; i < schemas.getLength(); i++) { + result.add(extractSchema((Element) schemas.item(i), + getNamespaceDeclarations(wsdl))); + } + return result; + } catch (Exception e) { + throw new RuntimeException("Could not extract embedded schemas from WSDL.", e); + } + } + + private static List getNamespaceDeclarations(Element element) { + var namespaces = new ArrayList(); + var attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + var attr = (Attr) attributes.item(i); + if (XMLNS_NS.equals(attr.getNamespaceURI())) { + namespaces.add(attr); + } + } + return namespaces; + } + + private static Document extractSchema(Element originalSchema, List definitionNamespaces) throws Exception { + var fac = DocumentBuilderFactory.newInstance(); + fac.setNamespaceAware(true); + var builder = fac.newDocumentBuilder(); + + var schema = builder.newDocument(); + var copiedSchema = (Element) schema.importNode(originalSchema, true); + schema.appendChild(copiedSchema); + + addMissingNamespaceDeclarations(copiedSchema, definitionNamespaces); + + return schema; + } + + private static void addMissingNamespaceDeclarations(Element schema, List definitionNamespaces) { + for (var nsDecl : definitionNamespaces) { + if (!schema.hasAttributeNS(XMLNS_NS, getNamespaceLocalName(nsDecl))) { + schema.setAttributeNS(XMLNS_NS, nsDecl.getName(), nsDecl.getValue()); + } + } + } + + private static String getNamespaceLocalName(Attr nsDecl) { + if (XMLNS_ATTRIBUTE.equals(nsDecl.getPrefix())) { + return nsDecl.getLocalName(); + } + return XMLNS_ATTRIBUTE; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidator.java index 98447a34fc..a36ca979a3 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidator.java @@ -15,221 +15,162 @@ package com.predic8.membrane.core.interceptor.schemavalidation; import com.predic8.membrane.core.exchange.*; -import com.predic8.membrane.core.http.Message; import com.predic8.membrane.core.http.*; +import com.predic8.membrane.core.http.Message; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.multipart.*; import com.predic8.membrane.core.resolver.*; import com.predic8.membrane.core.util.*; -import com.predic8.schema.*; -import com.predic8.wsdl.*; +import com.predic8.membrane.core.util.wsdl.parser.Definitions.*; import org.jetbrains.annotations.*; import org.slf4j.*; +import org.w3c.dom.*; +import org.xml.sax.*; import javax.xml.namespace.*; +import javax.xml.parsers.*; import javax.xml.transform.*; import java.io.*; import java.util.*; import static com.predic8.membrane.annot.Constants.SoapVersion.*; +import static com.predic8.membrane.core.http.Header.VALIDATION_ERROR_SOURCE; import static com.predic8.membrane.core.interceptor.Outcome.*; +import static com.predic8.membrane.core.interceptor.schemavalidation.WSDLMessageElementExtractor.*; +import static com.predic8.membrane.core.interceptor.schemavalidation.WSDLMessageElementExtractor.getPossibleRequestElements; import static com.predic8.membrane.core.util.SOAPUtil.FaultCode.*; import static com.predic8.membrane.core.util.SOAPUtil.*; -import static com.predic8.membrane.core.util.WSDLUtil.*; +import static com.predic8.membrane.core.util.wsdl.parser.Definitions.SOAPVersion.*; public class WSDLValidator extends AbstractXMLSchemaValidator { - private static final Logger log = LoggerFactory.getLogger(WSDLValidator.class.getName()); - - /** - * A WSDL can have several definition.service elements. The serviceName is the - * name of the service against to validate - */ - private final String serviceName; - - /** - * List of toplevel soapElements that are valid for requests - */ - private Set requestElements = new HashSet<>(); - - /** - * List of toplevel soapElements that are valid for responses - */ - private Set responseElements = new HashSet<>(); - - /** - * There might be additional toplevel Elements in a schema that are not used in - * a WSDL message. This field controls if it is checked if an element can be used as - * a request or response message - */ - private boolean checkIfSOAPElementIsUsedAsAWSDLMessage; - - /** - * Does WSDL supports SOAP Version 1.1? - */ - private boolean soap11; - - /** - * Does WSDL supports SOAP Version 1.1? - */ - private boolean soap12; - - public WSDLValidator(ResolverMap resourceResolver, String location, String serviceName, ValidatorInterceptor.FailureHandler failureHandler, boolean skipFaults) { - super(resourceResolver, location, failureHandler, skipFaults); - this.serviceName = serviceName; - } - - @Override - public String getName() { - return "wsdl-validator"; - } - - @Override - public Outcome validateMessage(Exchange exc, Interceptor.Flow flow) throws Exception { - Message msg = exc.getMessage(flow); - SOAPAnalysisResult result = SOAPUtil.analyseSOAPMessage( xopr, msg); - - if (result.version() == SOAP11 && !soap11) { - setErrorResponse(exc,"SOAP version 1.1 is not valid"); - return ABORT; - } - if (result.version() == SOAP12 && !soap12) { - setErrorResponse(exc,"SOAP version 1.2 is not valid"); - return ABORT; - } - - if (checkIfSOAPElementIsUsedAsAWSDLMessage) { - if (msg instanceof Request && !isPossibleRequestElement(result.soapElement())) { - setErrorResponse(exc,"%s is not a valid request element. Possible elements are %s".formatted(result.soapElement(), requestElements)); - return ABORT; - } - if (msg instanceof Response && !isPossibleResponseElement(result.soapElement())) { - setErrorResponse(exc,"%s is not a valid response element. Possible elements are %s".formatted(result.soapElement(), requestElements)); - return ABORT; - } - } - return super.validateMessage(exc, flow); - } - - private boolean isPossibleRequestElement(javax.xml.namespace.QName name) { - return isPossibleSOAPElement(requestElements, name); - } - - private boolean isPossibleResponseElement(javax.xml.namespace.QName name) { - return isPossibleSOAPElement(responseElements, name); - } - - private boolean isPossibleSOAPElement(Set elementNames, QName name) { - return elementNames.stream().anyMatch(qn -> qn.equals(name)); - } - - @Override - protected List getSchemas() { - WSDLParserContext ctx = new WSDLParserContext(); - ctx.setInput(location); - - Definitions definitions = parseWsdl(ctx); - readPossibleToplevelSOAPElements(definitions); - return getSchemas(definitions); - } - - private Definitions parseWsdl(WSDLParserContext ctx) { - Definitions definitions; - try { - definitions = getWsdlParser().parse(ctx); - } - catch (NoSuchElementException e) { - log.error(e.getMessage()); - throw new RuntimeException(); - } - catch (RuntimeException e) { - if (e.getCause() instanceof ResourceRetrievalException re) { - String msg = "Could not read WSDL from %s or its dependent XML Schemas.".formatted(location); - log.error(msg); - throw new IllegalStateException(msg, re); - } - log.error("Error downloading WSDL from {}.", location); - throw e; - } - return definitions; - } - - private static @NotNull List getSchemas(Definitions definitions) { - return definitions.getTypes().stream().map(Types::getSchemas).flatMap(List::stream).toList(); - } - - private void readPossibleToplevelSOAPElements(Definitions definitions) { - - if (serviceName != null) { - checkIfSOAPElementIsUsedAsAWSDLMessage = true; - Service service = WSDLUtil.getService(definitions, serviceName); - determinePossibleSoapVersions(service); - requestElements = getPossibleSOAPElements(service, Direction.REQUEST); - responseElements = getPossibleSOAPElements(service, Direction.RESPONSE); - return; - } - - // WSDL without a service element - if (definitions.getServices().isEmpty()) { - checkIfSOAPElementIsUsedAsAWSDLMessage = true; - // No binding information so allow all SOAP versions - soap11 = true; - soap12 = true; - definitions.getPortTypes().forEach(portType -> { - requestElements.addAll(getPossibleSOAPElements(portType, Direction.REQUEST)); - responseElements.addAll(getPossibleSOAPElements(portType, Direction.RESPONSE)); - }); - return; - } - - // Check what SOAP versions are declared in the WSDL - definitions.getServices().forEach(this::determinePossibleSoapVersions); - } - - private void determinePossibleSoapVersions(Service service) { - service.getPorts().forEach(port -> { - switch (getSOAPVersion(port)) { - case SOAP11: soap11 = true; break; - case SOAP12: soap12 = true; break; - } - }); - } - - private @NotNull WSDLParser getWsdlParser() { - WSDLParser parser = new WSDLParser(); - parser.setResourceResolver(resolver.toExternalResolver().toExternalResolver()); - return parser; - } - - @Override - protected Source getMessageBody(InputStream input) { - return MessageUtil.getSOAPBody(input); - } - - @Override - protected void setErrorResponse(Exchange exchange, String message) { - exchange.setResponse(SOAPUtil.createSOAPFaultResponse(Client, getErrorTitle(),Map.of("error",message))); - } - - @Override - protected void setErrorResponse(Exchange exchange, Interceptor.Flow flow, List exceptions) { - exchange.setResponse(createSOAPFaultResponse(Client, getErrorTitle(), Map.of("validation", convertExceptionsToMap(exceptions)))); - } - - @Override - protected boolean isFault(Message msg) { - return SOAPUtil.analyseSOAPMessage( xopr, msg).isFault(); - } - - @Override - protected String getPreliminaryError(XOPReconstitutor xopr, Message msg) { - if (isSOAP( xopr, msg)) - return null; - return "Not a SOAP message."; - } - - @Override - public String getErrorTitle() { - return "WSDL message validation failed"; - } -} + private static final Logger log = LoggerFactory.getLogger(WSDLValidator.class.getName()); + + /** + * List of toplevel soapElements that are valid for requests + */ + private final Set requestElements; + + /** + * List of toplevel soapElements that are valid for responses + */ + private final Set responseElements; + + private final Set versions; + + private final boolean skipFaults; + + /** + * Parsed WSDL document + */ + private final com.predic8.membrane.core.util.wsdl.parser.Definitions definitions; + + public WSDLValidator(ResolverMap resourceResolver, String location, String serviceName, ValidatorInterceptor.FailureHandler failureHandler, boolean skipFaults) { + super(resourceResolver, location, failureHandler); + this.skipFaults = skipFaults; + + try { + definitions = com.predic8.membrane.core.util.wsdl.parser.Definitions.parse(resourceResolver, location); + } catch (ResourceRetrievalException e) { + throw new ConfigurationException(""" + Could not extract embedded schemas from WSDL at location %s. + """.formatted(location), e); + } catch (Exception e) { + throw new ConfigurationException(""" + Could not parse WSDL as XML document at location %s. + Error Message: %s + """.formatted(location, e.getMessage())); + } + + requestElements = getPossibleRequestElements(definitions, serviceName); + responseElements = getPossibleResponseElements(definitions, serviceName); + versions = definitions.getSoapVersions(); + } + + @Override + public String getName() { + return "wsdl-validator"; + } + + @Override + public Outcome validateMessage(Exchange exc, Interceptor.Flow flow) throws Exception { + var message = exc.getMessage(flow); + var result = analyseSOAPMessage(xopr, message); + + if (!result.isSOAP()) { + setErrorResponse(exc, "Not a valid SOAP message."); + exc.getResponse().getHeader().add(VALIDATION_ERROR_SOURCE, flow.name()); + return ABORT; + } + + if (result.isFault() && skipFaults) { + log.debug("Skipping validation of fault message."); + return CONTINUE; + } + + if (!versions.isEmpty()) { + if (result.version() == SOAP11 && !versions.contains(SOAP_11)) { + setErrorResponse(exc, "SOAP version 1.1 is not valid"); + return ABORT; + } + if (result.version() == SOAP12 && !versions.contains(SOAP_12)) { + setErrorResponse(exc, "SOAP version 1.2 is not valid"); + return ABORT; + } + } + + if (message instanceof Request && !isPossibleRequestElement(result.soapElement())) { + setErrorResponse(exc, "%s is not a valid request element. Possible elements are %s".formatted(result.soapElement(), requestElements)); + return ABORT; + } + if (message instanceof Response && !isPossibleResponseElement(result.soapElement())) { + setErrorResponse(exc, "%s is not a valid response element. Possible elements are %s".formatted(result.soapElement(), responseElements)); + return ABORT; + } + return super.validateMessage(exc, flow); + } + + private boolean isPossibleRequestElement(javax.xml.namespace.QName name) { + return isPossibleSOAPElement(requestElements, name); + } + + private boolean isPossibleResponseElement(javax.xml.namespace.QName name) { + return isPossibleSOAPElement(responseElements, name); + } + + private boolean isPossibleSOAPElement(Set elementNames, QName name) { + return elementNames.contains(name); + } + + @Override + protected List getSchemas() { + return definitions.getSchemaElements(); + } + + @Override + protected Source getMessageBody(InputStream input) { + return MessageUtil.getSOAPBody(input); + } + + @Override + protected void setErrorResponse(Exchange exchange, String message) { + exchange.setResponse(createSOAPFaultResponse(Client, getErrorTitle(), Map.of("error", message))); + } + + @Override + protected void setErrorResponse(Exchange exchange, Interceptor.Flow flow, List exceptions) { + exchange.setResponse(createSOAPFaultResponse(Client, getErrorTitle(), Map.of("validation", convertExceptionsToMap(exceptions)))); + } + + @Override + protected String getPreliminaryError(XOPReconstitutor xopr, Message msg) { + if (isSOAP(xopr, msg)) + return null; + return "Not a SOAP message."; + } + + @Override + public String getErrorTitle() { + return "WSDL message validation failed"; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/XMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/XMLSchemaValidator.java index 8d04220ed6..8d4dc41dcf 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/XMLSchemaValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/XMLSchemaValidator.java @@ -20,8 +20,8 @@ import com.predic8.membrane.core.multipart.*; import com.predic8.membrane.core.resolver.*; import com.predic8.membrane.core.util.*; -import com.predic8.schema.Schema; import org.slf4j.*; +import org.w3c.dom.*; import org.xml.sax.*; import javax.xml.transform.*; @@ -32,10 +32,11 @@ import static com.predic8.membrane.annot.Constants.*; import static com.predic8.membrane.core.exceptions.ProblemDetails.*; -import static com.predic8.membrane.core.http.Header.VALIDATION_ERROR_SOURCE; +import static com.predic8.membrane.core.http.Header.*; public class XMLSchemaValidator extends AbstractXMLSchemaValidator { - private static final Logger log = LoggerFactory.getLogger(XMLSchemaValidator.class.getName()); + + private static final Logger log = LoggerFactory.getLogger(XMLSchemaValidator.class); public XMLSchemaValidator(ResolverMap resourceResolver, String location, ValidatorInterceptor.FailureHandler failureHandler) { super(resourceResolver, location, failureHandler); @@ -48,7 +49,7 @@ public String getName() { } @Override - protected List getSchemas() { + protected List getSchemas() { return null; // never gets called } @@ -109,11 +110,6 @@ protected void setErrorResponse(Exchange exchange, Interceptor.Flow flow, List @@ -89,29 +93,29 @@ public String rewrite(String path) { if (!path.contains("://") && !path.startsWith("/")) { path = combine(resource, path); } - synchronized (paths) { - if (paths_reverse.containsKey(path)) { - path = paths_reverse.get(path).toString(); - } else { - int n = paths.size() + 1; - paths.put(n, path); - paths_reverse.put(path, n); - documents_to_process.add(path); - path = Integer.toString(n); - } - } - path = "./" + URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), exc.getDestinations().getFirst()) + "?xsd=" + path; + return "./%s?xsd=%s".formatted(URLUtil.getNameComponent(router.getConfiguration().getUriFactory(), exc.getDestinations().getFirst()), resolveToNumber(path)); } catch (Exception e) { throw new RuntimeException(e); } - return path; + } + + private @NotNull String resolveToNumber(String path) { + synchronized (paths) { + if (pathsReverse.containsKey(path)) { + return pathsReverse.get(path).toString(); + } + int n = paths.size() + 1; + paths.put(n, path); + pathsReverse.put(path, n); + documentsToProcess.add(path); + return Integer.toString(n); + } } } @Override public void init() { super.init(); - webServerInterceptor = new WebServerInterceptor(); webServerInterceptor.init(router); @@ -123,10 +127,12 @@ public void init() { @GuardedBy("paths") private final Map paths = new HashMap<>(); + @GuardedBy("paths") - private final Map paths_reverse = new HashMap<>(); + private final Map pathsReverse = new HashMap<>(); + @GuardedBy("paths") - private final Queue documents_to_process = new LinkedList<>(); + private final Queue documentsToProcess = new LinkedList<>(); private void processDocuments(Exchange exc) { // exc.response is only temporarily used so we can call the WSDLInterceptor @@ -134,12 +140,12 @@ private void processDocuments(Exchange exc) { synchronized (paths) { try { while (true) { - String doc = documents_to_process.poll(); + var doc = documentsToProcess.poll(); if (doc == null) break; log.debug("processing: {}", doc); exc.setResponse(webServerInterceptor.createResponse(router.getResolverMap(), doc)); - WSDLInterceptor wi = new WSDLInterceptor(); + var wi = new WSDLInterceptor(); wi.setRewriteEndpoint(false); wi.setPathRewriter(new RelativePathRewriter(exc, doc)); wi.handleResponse(exc); @@ -163,9 +169,9 @@ public void setWsdl(String wsdl) { this.wsdl = wsdl; synchronized (paths) { paths.clear(); - paths_reverse.clear(); - documents_to_process.clear(); - documents_to_process.add(wsdl); + pathsReverse.clear(); + documentsToProcess.clear(); + documentsToProcess.add(wsdl); } } @@ -203,9 +209,9 @@ private Outcome handleRequestInternal(final Exchange exc) throws Exception { exc.getResponse().getHeader().setContentType(TEXT_XML); } if (exc.getRequest().getUri().contains("?xsd=")) { - Map params = URLParamUtil.getParams(router.getConfiguration().getUriFactory(), exc, URLParamUtil.DuplicateKeyOrInvalidFormStrategy.ERROR); + var params = getParams(exc); if (params.containsKey("xsd")) { - int n = Integer.parseInt(params.get("xsd")); + var n = Integer.parseInt(params.get("xsd")); String path; processDocuments(exc); synchronized (paths) { @@ -220,15 +226,15 @@ private Outcome handleRequestInternal(final Exchange exc) throws Exception { } } if (resource != null) { - WSDLInterceptor wi = new WSDLInterceptor(); + var wi = new WSDLInterceptor(); wi.setRewriteEndpoint(false); - wi.setPathRewriter(new RelativePathRewriter(exc, combine(router.getConfiguration().getBaseLocation(), wsdl))); + wi.setPathRewriter(new RelativePathRewriter(exc, combine(router.getConfiguration().getBaseLocation(), resource))); wi.init(router); wi.handleResponse(exc); return RETURN; } } catch (NumberFormatException e) { - exc.setResponse(HttpUtil.setHTMLErrorResponse(Response.internalServerError(), "Bad parameter format.", "")); + exc.setResponse(HttpUtil.setHTMLErrorResponse(Response.badRequest(), "Bad parameter format.", "")); return ABORT; } catch (ResourceRetrievalException e) { exc.setResponse(notFound().build()); @@ -238,6 +244,10 @@ private Outcome handleRequestInternal(final Exchange exc) throws Exception { return CONTINUE; } + private Map getParams(Exchange exc) throws URISyntaxException, IOException { + return URLParamUtil.getParams(router.getConfiguration().getUriFactory(), exc, ERROR); + } + private static boolean isRequestForWSDL(Exchange exc) { return exc.getRequest().getUri().endsWith("?wsdl") || exc.getRequest().getUri().endsWith("?WSDL"); } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/soap/WebServiceExplorerInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/soap/WebServiceExplorerInterceptor.java index 80a1422a02..0175031880 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/soap/WebServiceExplorerInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/soap/WebServiceExplorerInterceptor.java @@ -15,20 +15,14 @@ import com.googlecode.jatl.*; import com.predic8.membrane.annot.*; -import com.predic8.membrane.core.config.ProxyAware; +import com.predic8.membrane.core.config.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.administration.*; import com.predic8.membrane.core.interceptor.rest.*; import com.predic8.membrane.core.proxies.Proxy; -import com.predic8.membrane.core.resolver.*; -import com.predic8.membrane.core.proxies.*; -import com.predic8.membrane.core.util.*; -import com.predic8.wsdl.*; -import com.predic8.wstool.creator.*; -import groovy.xml.MarkupBuilder; -import org.jetbrains.annotations.*; +import com.predic8.membrane.core.util.wsdl.parser.*; import org.slf4j.*; import java.io.*; @@ -37,7 +31,7 @@ import java.util.regex.*; import static com.predic8.membrane.annot.Constants.*; -import static com.predic8.membrane.core.http.Response.ok; +import static com.predic8.membrane.core.http.Response.*; import static com.predic8.membrane.core.interceptor.Outcome.*; import static java.util.regex.Pattern.*; @@ -47,12 +41,16 @@ @MCElement(name="webServiceExplorer") public class WebServiceExplorerInterceptor extends RESTInterceptor implements ProxyAware { - private static final Logger log = LoggerFactory.getLogger(WebServiceExplorerInterceptor.class.getName()); + private static final Logger log = LoggerFactory.getLogger(WebServiceExplorerInterceptor.class); private static final Pattern wsdlRequest = compile(".*\\?(wsdl|xsd=.*)", CASE_INSENSITIVE); private String wsdl; private String portName; + + /** + * Field is accessed by reflection + */ private Proxy proxy; public WebServiceExplorerInterceptor() { @@ -80,7 +78,6 @@ public String getWsdl() { @MCAttribute public void setWsdl(String wsdl) { this.wsdl = wsdl; - this.parsedWSDL = null; } public String getPortName() { @@ -92,69 +89,11 @@ public void setPortName(String portName) { this.portName = portName; } - private volatile Definitions parsedWSDL; - - private Definitions getParsedWSDL() { - if (parsedWSDL != null) - return parsedWSDL; - WSDLParserContext ctx = new WSDLParserContext(); - ctx.setInput(ResolverMap.combine(router.getConfiguration().getBaseLocation(), wsdl)); - return parsedWSDL = getWsdlParser().parse(ctx); - } - - private @NotNull WSDLParser getWsdlParser() { - WSDLParser wsdlParser = new WSDLParser(); - wsdlParser.setResourceResolver(router.getResolverMap().toExternalResolver().toExternalResolver()); - return wsdlParser; - } - - @Mapping("[^?]*/operation/([^/?]+)/([^/?]+)/([^/?]+)") - public Response createOperationResponse(QueryParameter params, String relativeRootPath) { - try { - final String bindingName = params.getGroup(1); - final String portName = params.getGroup(2); - final String operationName = params.getGroup(3); - - final Service service = getService(getParsedWSDL()); - - StringWriter sw = new StringWriter(); - new StandardPage(sw, null) { - @Override - protected void createContent() { - h1().text("Service Proxy for " + service.getName()); - h2().text("Operation: " + operationName).end(); - - h3().text("Sample Request").end(); - - pre().text(generateSampleRequest(portName, operationName, bindingName, getParsedWSDL())).end(); - } - }; - return ok(sw.toString()).build(); - } catch (IllegalArgumentException e) { - log.error("", e); - return Response.internalServerError().build(); - } - } - - private Service getService(Definitions d) { - - if (proxy instanceof SOAPProxy sp) { - String serviceName = sp.getServiceName(); - if (serviceName != null) { - return WSDLUtil.getService(d, serviceName); - } - } - - if (d.getServices().size() != 1) - throw new IllegalArgumentException("WSDL needs to have exactly one service for SOAPUIInterceptor to work."); - return d.getServices().getFirst(); - } - private String getClientURL(Exchange exc) { // TODO: move this to some central location try { - String uri = exc.getHandler().getContextPath(exc) + exc.getRequest().getUri(); - String host = exc.getRequest().getHeader().getHost(); + var uri = exc.getHandler().getContextPath(exc) + exc.getRequest().getUri(); + var host = exc.getRequest().getHeader().getHost(); if (host != null) { if (host.contains(":")) host = host.substring(0, host.indexOf(":")); @@ -174,39 +113,31 @@ private String getClientURL(Exchange exc) { @Mapping("(?!.*operation)([^?]*)") public Response createSOAPUIResponse(QueryParameter params, final String relativeRootPath, final Exchange exc) throws Exception { try { - final String myPath = router.getConfiguration().getUriFactory().create(exc.getRequest().getUri()).getPath(); + var definitions = Definitions.parse(router.getResolverMap(), wsdl); - final Definitions w = getParsedWSDL(); - final Service service = getService(w); - final Port port = SOAPProxy.selectPort(service.getPorts(), portName); - final List ports = getPortsByLocation(service, port); + var service = definitions.getServices().getFirst(); // TODO display all services + var port = service.getPorts().getFirst(); + var ports = service.getPorts(); - StringWriter sw = new StringWriter(); + var sw = new StringWriter(); new StandardPage(sw, service.getName()) { @Override protected void createContent() { h1().text("Service Proxy: " + service.getName()).end(); p(); - text("Target Namespace: " + w.getTargetNamespace()); + text("Target Namespace: " + definitions.getTargetNamespace()); br().end(); String wsdlLink = getClientURL(exc) + "?wsdl"; text("WSDL: ").a().href(wsdlLink).text(wsdlLink).end(); end(); - for (PortType pt : WSDLUtil.getPortTypes(service)) { + for (com.predic8.membrane.core.util.wsdl.parser.PortType pt : ports.stream().map(Port::getBinding).map(Binding::getPortType).toList()) { h2().text("Port Type: " + pt.getName()).end(); - Documentation d = pt.getDocumentation(); - if (d != null) { - p().text("Documentation: " + d).end(); - } + // @TODO show pt.getDocumentation() wsdl:documentation here when it is implemented } - Binding binding = port.getBinding(); - List bindingOperations = getOperationsByBinding(w, binding); - if (bindingOperations.isEmpty()) - p().text("There are no operations defined.").end(); - else - createOperationsTable(w, bindingOperations, binding, binding.getPortType()); + var binding = port.getBinding(); + createOperationsTable(definitions, binding, binding.getPortType()); h2().text("Virtual Endpoint").end(); p().a().href(getClientURL(exc)).text(getClientURL(exc)).end().end(); @@ -218,30 +149,30 @@ protected void createContent() { createEndpointTable(service.getPorts(), ports); } - private void createOperationsTable(Definitions w, List bindingOperations, Binding binding, PortType portType) { + private void createOperationsTable(Definitions defs, Binding binding, PortType portType) { table().cellspacing("0").cellpadding("0").border(""+1); tr(); - th().text("Operation").end(); - th().text("Input").end(); - th().text("Output").end(); + th().text("Operation").end(); + th().text("Input").end(); + th().text("Output").end(); + th().text("Fault").end(); end(); - for (Operation o : bindingOperations) { + for (Operation o : portType.getOperations()) { tr(); td(); - if ("HTTP".equals(getProtocolVersion(binding))) { text(o.getName()); - } else { - a().href(getLinkForOperation(binding, portType, o, myPath)).text(o.getName()).end(); - } end(); td(); - for (Part p : o.getInput().getMessage().getParts()) - text(p.getElement().getName()); - + for (Part p : o.getInputs().stream().map(om -> om.getMessage().getPart()).toList()) + text(p.getElementQName().toString()); + end(); + td(); + for (Part p : o.getOutputs().stream().map(om -> om.getMessage().getPart()).toList()) + text(p.getElementQName().toString()); end(); td(); - for (Part p : o.getOutput().getMessage().getParts()) - text(p.getElement().getName()); + for (Part p : o.getFaults().stream().map(om -> om.getMessage().getPart()).toList()) + text(p.getElementQName().toString()); end(); end(); } @@ -255,10 +186,10 @@ private void createEndpointTable(List ports, List matchingPorts) { th().text("Protocol").end(); th().text("URL").end(); end(); - for (Port p : ports) { + for (com.predic8.membrane.core.util.wsdl.parser.Port p : ports) { tr(); td().text(p.getName()).end(); - td().text(getProtocolVersion(p.getBinding())).end(); + td().text(p.getBinding().getSoapVersion().name()).end(); td().text(p.getAddress().getLocation()).end(); td(); if (matchingPorts.contains(p)) @@ -278,10 +209,6 @@ private void createEndpointTable(List ports, List matchingPorts) { } } - private String getLinkForOperation(Binding binding, PortType portType, Operation o, String path) { - return path + "/operation/" + binding.getName() + "/" + portType.getName() + "/" + o.getName(); - } - private abstract static class StandardPage extends Html { public StandardPage(Writer writer, String title) { @@ -316,49 +243,6 @@ public StandardPage(Writer writer, String title) { protected abstract void createContent(); } - private List getOperationsByBinding(final Definitions w, Binding binding) { - List bindingOperations = new ArrayList<>(); - for (Operation o : w.getOperations()) - if (binding.getOperation(o.getName()) != null) - bindingOperations.add(o); - return bindingOperations; - } - - private List getPortsByLocation(Service service, Port port) { - String location = port.getAddress().getLocation(); - if (location == null) - throw new IllegalArgumentException("Location not set for port in WSDL."); - - final List ports = new ArrayList<>(); - for (Port p : service.getPorts()) - if (location.equals(p.getAddress().getLocation())) - ports.add(p); - return ports; - } - - private String getProtocolVersion(Binding binding) { - String transport = getNamespaceURI(binding); - if (WSDL_SOAP11_NS.equals(transport)) - transport = "SOAP 1.1"; - if (WSDL_SOAP12_NS.equals(transport)) - transport = "SOAP 1.2"; - if (WSDL_HTTP_NS.equals(transport)) - transport = "HTTP"; - return transport; - } - - private String getNamespaceURI(Binding binding) { - return ((javax.xml.namespace.QName) binding.getBinding().getElementName()).getNamespaceURI(); - } - - private String generateSampleRequest(final String portName, final String operationName, - final String bindingName, final Definitions w) { - StringWriter writer = new StringWriter(); - SOARequestCreator creator = new SOARequestCreator(w, new RequestTemplateCreator(), new MarkupBuilder(writer)); - creator.createRequest(portName, operationName, bindingName); - return writer.toString(); - } - @Override public String getShortDescription() { return "Displays a graphical UI describing the web service when accessed using GET requests."; 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 6c976d7ce1..3001840812 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,6 +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.rewrite.RewriteInterceptor.*; import com.predic8.membrane.core.interceptor.schemavalidation.*; import com.predic8.membrane.core.interceptor.server.*; import com.predic8.membrane.core.interceptor.soap.*; @@ -26,17 +27,15 @@ 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 com.predic8.membrane.core.util.wsdl.parser.*; import org.apache.commons.lang3.*; import org.jetbrains.annotations.*; import org.slf4j.*; import java.net.*; -import java.util.*; import java.util.regex.*; -import static com.predic8.membrane.core.interceptor.InterceptorUtil.moveToFirstPosition; +import static com.predic8.membrane.core.interceptor.InterceptorUtil.*; /** * @description

@@ -59,8 +58,6 @@ @MCElement(name = "soapProxy", topLevel = true, component = false) public class SOAPProxy extends AbstractServiceProxy { - private static final Logger log = LoggerFactory.getLogger(SOAPProxy.class.getName()); - // configuration attributes protected String wsdl; protected String portName; @@ -84,7 +81,7 @@ public void init() { configureFromWSDL(); super.init(); // Must be called last! Otherwise, SSL will not be configured! - for (Interceptor interceptor : interceptors) { + for (var interceptor : interceptors) { if (interceptor instanceof WSDLPublisherInterceptor wpi) { wpi.setSoapProxy(this); } else if (interceptor instanceof ValidatorInterceptor vi) { @@ -94,12 +91,10 @@ public void init() { } protected void configureFromWSDL() { - - Definitions definitions = parseWSDLOnly(); - Service service = getService(definitions); - setProxyName(service, definitions); - - String location = getLocation(service); + var defs = parseWSDL(); + var service = getService(defs); + setProxyName(service, defs); + var location = getLocation(service); // Signal to the later processing that the outgoing connection is using TLS if (location.startsWith("https")) { @@ -116,19 +111,33 @@ protected void configureFromWSDL() { wsdlInterceptor.setPathRewriterOnWSDLInterceptor(key.getPath()); } - private Definitions parseWSDLOnly() { + private Service getService(Definitions defs) { + if (serviceName != null) + return defs.getService(serviceName).orElseThrow( + () -> new ConfigurationException("No service with name '%s' found in WSDL %s".formatted(serviceName, wsdl)) + ); + if (defs.getServices().isEmpty()) + throw new ConfigurationException("No service element found in WSDL %s".formatted(wsdl)); + return defs.getServices().getFirst(); + } + + private @NotNull Definitions parseWSDL() { try { - return getWsdlParser().parse(getWsdlParserContext()); + return Definitions.parse(resolverMap, wsdl); } catch (Exception e) { - String msg = "Could not parse WSDL from %s.".formatted(getWsdlParserContext().getInput()); - log.error("{}: {}", msg, e.getMessage()); - throw new ConfigurationException(msg, e); + throw new ConfigurationException(""" + Cannot parse WSDL + + API: %s + WSDL location: %s. + Error. %s + """.formatted( name, wsdl, e.getMessage())); } } private void prepareRouting(String location) { try { - URL url = new URL(location); + var url = new URL(location); setTarget(url); // Set target URL from WSDL location if (key.getPath() == null) { // If the config does not contain a path, use the path from the WSDL(address/@location) for the proxy key key.setUsePathPattern(true); @@ -148,8 +157,8 @@ private void configureRewritingOfPath(String targetPath) { if (targetPath == null) return; - RewriteInterceptor ri = new RewriteInterceptor(); - ri.setMappings(Lists.newArrayList(new RewriteInterceptor.Mapping("^" + Pattern.quote(key.getPath()), Matcher.quoteReplacement(targetPath), "rewrite"))); + var ri = new RewriteInterceptor(); + ri.setMappings(Lists.newArrayList(new Mapping("^" + Pattern.quote(key.getPath()), Matcher.quoteReplacement(targetPath), "rewrite"))); interceptors.addFirst(ri); } @@ -160,53 +169,14 @@ private void configureRewritingOfPath(String targetPath) { return url.getPath(); } - private @NotNull String getLocation(Service service) { - String location = getPort(service).getAddress().getLocation(); + private @NotNull String getLocation(com.predic8.membrane.core.util.wsdl.parser.Service service) { + var location = service.getPorts().getFirst().getAddress().getLocation(); if (location == null) throw new ConfigurationException("In the WSDL %s, there is no @location defined on the port.".formatted(wsdl)); return location; } - private @NotNull Port getPort(Service service) { - return selectPort(service.getPorts(), portName); - } - - private @NotNull WSDLParserContext getWsdlParserContext() { - WSDLParserContext ctx = new WSDLParserContext(); - ctx.setInput(ResolverMap.combine(router.getConfiguration().getBaseLocation(), wsdl)); - return ctx; - } - - private @NotNull WSDLParser getWsdlParser() { - WSDLParser wsdlParser = new WSDLParser(); - wsdlParser.setResourceResolver(resolverMap.toExternalResolver().toExternalResolver()); - return wsdlParser; - } - - private Service getService(Definitions definitions) { - List services = definitions.getServices(); - if (services.size() == 1) - return services.getFirst(); - if (serviceName == null) { - throw new SOAPProxyMultipleServicesException(this, getServiceNames(services)); - } - return getServiceByName(services, serviceName); - } - - private Service getServiceByName(List services, String serviceName) { - return services.stream() - .filter(s -> s.getName().equals(serviceName)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No service with name %s found in the WSDL. Available services are: %s".formatted(serviceName, getServiceNames(services)))); - } - - private static @NotNull List getServiceNames(List services) { - return services.stream() - .map(WSDLElement::getName) - .toList(); - } - private void setTarget(URL url) { if (wsdl.startsWith("internal:")) { try { @@ -229,16 +199,6 @@ private void setProxyName(Service service, Definitions definitions) { name = StringUtils.isEmpty(service.getName()) ? definitions.getName() : service.getName(); } - public static Port selectPort(List ports, String portName) { - if (portName != null) { - for (Port port : ports) - if (portName.equals(port.getName())) - return port; - throw new IllegalArgumentException("No port with name '" + portName + "' found."); - } - return WSDLUtil.getPort(ports); - } - private WSDLInterceptor addAndGetWSDLInterceptor() { return moveToFirstPosition(interceptors, WSDLInterceptor.class, WSDLInterceptor::new).orElseThrow(); } diff --git a/core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java b/core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java index 78629f4911..b7a022debc 100644 --- a/core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java +++ b/core/src/main/java/com/predic8/membrane/core/resolver/ResolverMap.java @@ -23,7 +23,6 @@ import com.predic8.membrane.core.transport.http.client.HttpClientConfiguration; import com.predic8.membrane.core.util.*; import com.predic8.membrane.core.util.functionalInterfaces.*; -import com.predic8.xml.util.*; import org.slf4j.*; import org.w3c.dom.ls.*; @@ -268,46 +267,9 @@ public LSResourceResolver toLSResourceResolver() { }; } - public ExternalResolverConverter toExternalResolver() { - return new ExternalResolverConverter(); - } public KubernetesSchemaResolver getKubernetesSchemaResolver() { return (KubernetesSchemaResolver) getSchemaResolver("kubernetes:"); } - public class ExternalResolverConverter { - - public ExternalResolver toExternalResolver() { - return new ExternalResolver() { - @Override - public InputStream resolveAsFile(String filename, String baseDir) { - try { - if (baseDir != null) { - return ResolverMap.this.resolve(combine(baseDir, filename)); - } - return ResolverMap.this.resolve(filename); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - protected InputStream resolveViaHttp(Object url) { - try { - String url2 = (String) url; - int q = url2.indexOf('?'); - if (q == -1) - url2 = url2.replaceAll("/[^/]+/\\.\\./", "/"); - else - url2 = url2.substring(0, q).replaceAll("/[^/]+/\\.\\./", "/") + url2.substring(q); - - return getSchemaResolver(url2).resolve(url2); - } catch (ResourceRetrievalException e) { - throw new RuntimeException(e); - } - } - }; - } - } } diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/TLSUnrecognizedNameException.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/TLSUnrecognizedNameException.java index d6bbe3c36b..dd5fc8309e 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/TLSUnrecognizedNameException.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/TLSUnrecognizedNameException.java @@ -15,7 +15,7 @@ import java.io.*; -import static groovy.json.StringEscapeUtils.*; +import static org.apache.commons.text.StringEscapeUtils.*; public class TLSUnrecognizedNameException extends IOException { public TLSUnrecognizedNameException(String hostname) { diff --git a/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java b/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java index 9a3850e028..e11d2fe478 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java @@ -102,25 +102,28 @@ public record SOAPAnalysisResult(boolean isSOAP, boolean isFault, SoapVersion ve } public static SOAPAnalysisResult analyseSOAPMessage( XOPReconstitutor xopr, Message msg) { + log.debug("Analyzing SOAP message: {}", msg); + /* * 0: waiting for "" * 1: waiting for "" (skipping any "") * 2: waiting for "" */ try { - XMLInputFactory f = XMLInputFactory.newInstance(); + var f = XMLInputFactory.newInstance(); f.setProperty(IS_REPLACING_ENTITY_REFERENCES, false); f.setProperty(IS_SUPPORTING_EXTERNAL_ENTITIES, false); - XMLEventReader parser = f.createXMLEventReader(xopr.reconstituteIfNecessary(msg)); + var parser = f.createXMLEventReader(xopr.reconstituteIfNecessary(msg)); SoapVersion version = null; int state = 0; while (parser.hasNext()) { - XMLEvent event = parser.nextEvent(); + var event = parser.nextEvent(); if (event.isStartElement()) { - QName name = ((StartElement) event).getName(); + var name = ((StartElement) event).getName(); if (state < 2 && !isSOAP11Element(name) && !isSOAP12Element(name)) { + log.debug("Not a SOAP Element: {}", name); return NO_SOAP_RESULT; } @@ -167,6 +170,7 @@ public static SOAPAnalysisResult analyseSOAPMessage( XOPReconstitutor xopr, Mess } catch (Exception e) { log.warn("Ignoring exception: ", e); } + log.debug("No SOAP Element found."); return NO_SOAP_RESULT; } diff --git a/core/src/main/java/com/predic8/membrane/core/util/URIUtil.java b/core/src/main/java/com/predic8/membrane/core/util/URIUtil.java index 5d3780a802..0d9e4761a5 100644 --- a/core/src/main/java/com/predic8/membrane/core/util/URIUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/util/URIUtil.java @@ -15,8 +15,9 @@ import org.jetbrains.annotations.*; -import java.net.URI; import java.net.*; +import java.net.URI; +import java.nio.file.*; import java.util.*; import java.util.regex.*; @@ -27,6 +28,7 @@ public class URIUtil { private static final Pattern driveLetterPattern = Pattern.compile("^(\\w)[/:|].*"); + private static final Pattern URI_SCHEME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*:.*"); /** * @@ -35,11 +37,11 @@ public class URIUtil { * @throws URISyntaxException */ public static java.net.URI convertPath2FileURI(String path) throws URISyntaxException { - return new URI( addFilePrefix(encodePathCharactersForUri(path))); + return new URI(addFilePrefix(encodePathCharactersForUri(path))); } public static String encodePathCharactersForUri(String s) { - return s.replaceAll(" ","%20").replace("\\","/"); + return s.replaceAll(" ", "%20").replace("\\", "/"); } private static @NotNull String addFilePrefix(String path) { @@ -51,8 +53,7 @@ public static String encodePathCharactersForUri(String s) { } public static String convertPath2FilePathString(String path) { - path = addFilePrefix(path); - return path.replaceAll(" ","%20").replaceAll("\\\\","/"); + return encodePathCharactersForUri(addFilePrefix(path)); } /** @@ -102,7 +103,7 @@ static String removeLeadingSlashes(String s) { } static Optional getPossibleDriveLetter(String p) { - Matcher m = driveLetterPattern.matcher(p); + var m = driveLetterPattern.matcher(p); if (m.matches()) { return Optional.of(m.group(1)); } @@ -138,4 +139,44 @@ public static String normalizeSingleDot(String uri) { return sb.toString(); } + /** + * Normalizes the given path or URI and resolves it to an absolute path. + * This method handles various formats of paths, + * including filesystem paths, URIs, and paths with potential Windows drive letters. + * The normalization involves resolving relative components, such as "." or "..", + * and ensuring the path conforms to a standardized format. + * + * @param location the path or URI string to normalize. + * @return the normalized absolute path or URI as a string. + * @throws IllegalArgumentException if the input location is null, empty, or contains malformed URI syntax. + */ + public static String getNormalizedAbsolutePathOrUri(String location) { + if (location == null || location.isEmpty()) + throw new IllegalArgumentException("location must not be null or empty"); + + // Windows drive letter path (e.g., C:\foo or C:/foo) + if (location.length() >= 2 + && Character.isLetter(location.charAt(0)) + && location.charAt(1) == ':') { + return normalizeInternal(location); + } + + // already absolute URI + if (URI_SCHEME_PATTERN.matcher(location).matches()) { + return URI.create(location).normalize().toString(); + } + + // ? (Query String) or # (Fragment) are hints of URLs, not filesystem paths + if (location.contains("?") || location.contains("#") || location.startsWith("//")) { + return URI.create(location).normalize().toString(); + } + + // filesystem path + return normalizeInternal(location); + } + + private static @NotNull String normalizeInternal(String location) { + return Path.of(location).toAbsolutePath().normalize().toString(); + } + } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java b/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java deleted file mode 100644 index ce99e9507b..0000000000 --- a/core/src/main/java/com/predic8/membrane/core/util/WSDLUtil.java +++ /dev/null @@ -1,109 +0,0 @@ -/* Copyright 2024 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.core.util; - -import com.predic8.membrane.annot.Constants; -import com.predic8.wsdl.*; -import org.slf4j.*; - -import javax.xml.namespace.*; -import java.util.*; - -import static com.predic8.membrane.annot.Constants.SoapVersion.*; -import static com.predic8.membrane.annot.Constants.WSDL_SOAP11_NS; -import static com.predic8.membrane.annot.Constants.WSDL_SOAP12_NS; -import static com.predic8.membrane.core.util.WSDLUtil.Direction.*; -import static com.predic8.membrane.core.util.xml.XMLUtil.groovyToJavaxQName; - -public class WSDLUtil { - - static final Logger log = LoggerFactory.getLogger(WSDLUtil.class.getName()); - - /** - * Searches a service in a WSDL. Does not consider namespaces - * - * @param definitions Parsed WSDL - * @param serviceName local name component of the attribute definitions.service.@name - * @return Service - */ - public static Service getService(Definitions definitions, String serviceName) { - List services = definitions.getServices().stream().filter(s -> s.getName().equals(serviceName)).toList(); - if (services.isEmpty()) { - String msg = "No service found with name %s in WSDL with namespace %s".formatted(serviceName, definitions.getTargetNamespace()); - log.error(msg); - throw new RuntimeException(msg); - } - return services.get(0); - } - - public static Set getPortTypes(Service service) { - Set portTypes = new HashSet<>(); - service.getPorts().forEach(port -> portTypes.add(port.getBinding().getPortType())); - return portTypes; - } - - public enum Direction {REQUEST, RESPONSE} - - /** - * A schema can have lots of toplevel elements but not all elements are valid toplevel elements in SOAP requests. - * Only the elements referenced from service->port->binding->porttype->msg->types-schema->element are valid - * toplevel elements in SOAP requests. - * - * @param service service for that elements should be searched - * @return Set of elements that are allowed in this service as soap elements - */ - public static Set getPossibleSOAPElements(Service service, Direction direction) { - return getPossibleSOAPElements(service.getPorts().get(0).getBinding().getPortType(), direction); - } - - public static Set getPossibleSOAPElements(PortType portType, Direction direction) { - Set elements = new HashSet<>(); - - portType.getOperations().forEach(op -> { - AbstractPortTypeMessage msg = getMessage(direction, op); - - // Does the Operation have an input/output message according to the direction parameter? - if (msg == null) - return; - - msg.getMessage().getParts().forEach(part -> elements.add(groovyToJavaxQName(part.getElement().getQname()))); - }); - - return elements; - } - - - /** - * SOAP version of a WSDL port - * @param port of definitions.service object - * @return SOAP version enum - */ - public static Constants.SoapVersion getSOAPVersion(Port port) { - if (port.getAddress().getElementName() instanceof QName qn) { - return switch (qn.getNamespaceURI()) { - case WSDL_SOAP11_NS -> SOAP11; - case WSDL_SOAP12_NS -> SOAP12; - default -> UNKNOWN; - }; - } - return UNKNOWN; - - } - - private static AbstractPortTypeMessage getMessage(Direction direction, Operation operation) { - if (direction == REQUEST) - return operation.getInput(); - return operation.getOutput(); - } -} 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 6fac93896b..59e60fb531 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 @@ -14,20 +14,14 @@ package com.predic8.membrane.core.util.soap; -import com.predic8.wsdl.*; import org.slf4j.*; -import javax.xml.namespace.*; -import java.util.*; import java.util.regex.*; -import static com.predic8.membrane.annot.Constants.*; -import static java.util.regex.Matcher.quoteReplacement; +import static java.util.regex.Matcher.*; 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) { @@ -35,41 +29,4 @@ public static String rewriteRelativeWsdlPath(String path, String replacementName .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/util/wsdl/parser/Address.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Address.java new file mode 100644 index 0000000000..3fec2b1a2f --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Address.java @@ -0,0 +1,28 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +public class Address extends WSDLElement { + + public Address(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public String getLocation() { + return getAttribute("location"); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Binding.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Binding.java new file mode 100644 index 0000000000..67768bea2b --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Binding.java @@ -0,0 +1,71 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import com.predic8.membrane.core.util.wsdl.parser.Definitions.*; +import org.w3c.dom.*; + +import java.util.*; + +import static com.predic8.membrane.core.util.wsdl.parser.WSDLParserUtil.*; + +public class Binding extends WSDLElement { + + public enum Style { + RPC, DOCUMENT; + + public static Style fromString(String style) { + return switch (style) { + case "rpc" -> RPC; + default -> DOCUMENT; + }; + } + } + + public Binding(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public Style getStyle() { + return getBindingStyle().getStyle(); + } + + public SOAPVersion getSoapVersion() { + return getBindingStyle().getSoapVersion(); + } + + public BindingOperation getBindingOperation(String name) { + return getBindingOperations().stream() + .filter(bo -> bo.getName().equals(name)) + .findFirst().orElseThrow(() -> new WSDLParserException("No bindingOperation found for name: " + name)); + } + + public List getBindingOperations() { + return instantiateWSDLChildren("operation", BindingOperation.class); + } + + private BindingStyle getBindingStyle() { + var binding = instantiateChildren("binding", BindingStyle.class); + if (binding.isEmpty()) + throw new WSDLParserException("No bindingStyle found for binding: " + getName()); + return binding.getFirst(); + } + + public PortType getPortType() { + return ctx.definitions().getPortTypes().stream() + .filter(pt -> getLocalName(getAttribute("type")).equals(pt.getName())) + .findFirst().orElseThrow(() -> new WSDLParserException("No portType found for binding: " + getName())); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingOperation.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingOperation.java new file mode 100644 index 0000000000..dd1e82bc10 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingOperation.java @@ -0,0 +1,30 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +public class BindingOperation extends WSDLElement{ + + public BindingOperation(WSDLParserContext ctx, Node node) { + super(ctx,node); + } + + public String getSoapAction() { + return instantiateChild("operation",ProtocolOperation.class).orElseThrow(() -> + new WSDLParserException("No operation found for binding operation: " + getName()) + ).getSoapAction(); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingStyle.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingStyle.java new file mode 100644 index 0000000000..e1766cb290 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/BindingStyle.java @@ -0,0 +1,32 @@ +package com.predic8.membrane.core.util.wsdl.parser; + +import com.predic8.membrane.core.util.wsdl.parser.Binding.*; +import com.predic8.membrane.core.util.wsdl.parser.Definitions.*; +import org.slf4j.*; +import org.w3c.dom.*; + +import static com.predic8.membrane.core.util.wsdl.parser.Definitions.SOAPVersion.*; + +public class BindingStyle extends WSDLElement { + + private static final Logger log = LoggerFactory.getLogger(BindingStyle.class); + + public BindingStyle(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public Style getStyle() { + return Style.fromString(getAttribute("style")); + } + + public SOAPVersion getSoapVersion() { + if (isWsdlSoap11Element()) { + return SOAP_11; + } + if (isWsdlSoap12Element()) { + return SOAP_12; + } + log.info("Unknown protocol of binding element in namespace: {}", this.element.getNamespaceURI()); + return UNKNOWN; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Definitions.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Definitions.java new file mode 100644 index 0000000000..cd5ae10cbc --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Definitions.java @@ -0,0 +1,134 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.wsdl.parser.schema.*; +import org.slf4j.*; +import org.w3c.dom.*; + +import java.util.*; + +import static java.util.stream.Collectors.*; + +/** + * WSDL elements register themselves via WSDLParserContext. This is more convenient, e.g. binding is not + * directly created. + */ +public class Definitions extends WSDLElement { + + private static final Logger log = LoggerFactory.getLogger(Definitions.class); + + public enum SOAPVersion { + SOAP_11, SOAP_12, UNKNOWN + } + + private List schemas = new ArrayList<>(); + private List messages = new ArrayList<>(); + private List portTypes = new ArrayList<>(); + private List bindings = new ArrayList<>(); + private List services = new ArrayList<>(); + + private String targetNamespace; + + private Set soapVersions = new HashSet<>(); + + private Definitions(Resolver resolver, String location) throws Exception { + super(new WSDLParserContext(null, resolver, location, new ArrayList<>()), read(resolver, location)); + ctx = ctx.definitions(this); + parse(); + } + + private static Node read(Resolver resolver, String location) throws Exception { + try (var is = resolver.resolve(location)) { + return WSDLParserUtil.parse(is); + } + } + + public static Definitions parse(Resolver resolver, String location) throws Exception { + log.debug("Parsing WSDL from {}", location); + return new Definitions(resolver, location); + } + + private void parse() { + targetNamespace = getAttribute("targetNamespace"); + schemas = instantiateXSDElements(getTypes().element, "schema", Schema.class); + importEmbeddedSchemas(); + messages = instantiateWSDLChildren("message", Message.class); + portTypes = instantiateWSDLChildren("portType", PortType.class); + bindings = instantiateWSDLChildren( "binding", Binding.class); + services = instantiateWSDLChildren( "service", Service.class); + soapVersions = bindings.stream().map(Binding::getSoapVersion).collect(toSet()); + } + + /** + * Go through all embedded schemas and import all embedded schemas that are referenced there + */ + private void importEmbeddedSchemas() { + schemas.forEach(s -> s.getImports() + .forEach(i -> i.importEmbeddedSchema(this))); + } + + public Optional getEmbeddedSchema(String namespace) { + return schemas.stream() + .filter(s -> Objects.equals(s.getTargetNamespace(), namespace)) + .findFirst(); + } + + public Types getTypes() { + return instantiateChild("types", Types.class) + .orElseThrow(() -> new WSDLParserException("No types element found in WSDL")); + } + + public List getSchemas() { + return schemas; + } + + public List getSchemaElements() { + return schemas.stream().map(Schema::getSchemaElement).toList(); + } + + public List getMessages() { + return messages; + } + + public Optional findMessage(String name) { + return messages.stream().filter(m -> m.getName().equals(name)).findFirst(); + } + + public List getPortTypes() { + return portTypes; + } + + public List getBindings() { + return bindings; + } + + public List getServices() { + return services; + } + + public Optional getService(String name) { + return services.stream().filter(s -> s.getName().equals(name)).findFirst(); + } + + public String getTargetNamespace() { + return targetNamespace; + } + + public Set getSoapVersions() { + return soapVersions; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Message.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Message.java new file mode 100644 index 0000000000..df4c4a4fae --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Message.java @@ -0,0 +1,43 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import java.util.*; + +public class Message extends WSDLElement { + + public Message(WSDLParserContext ctx, Node node) { + super(ctx,node); + } + + /** + * Document style only uses one part. + * @return + */ + public Part getPart() { + return getParts().getFirst(); + } + + public List getParts() { + return instantiateWSDLChildren("part", Part.class); + } + + @Override + public String toString() { + return "Message [parts=%s]".formatted(getParts()); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Operation.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Operation.java new file mode 100644 index 0000000000..7ef9f9a98d --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Operation.java @@ -0,0 +1,83 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import java.util.*; + +public class Operation extends WSDLElement { + + public enum Direction { + INPUT, OUTPUT, FAULT; + + public boolean matches(String s) { + return name().equalsIgnoreCase(s); + } + } + + public Operation(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public List getInputs() { + return instantiateChildren("input", Input.class); + } + + public List getOutputs() { + return instantiateChildren("output", Output.class); + } + + public List getFaults() { + return instantiateChildren("fault", Fault.class); + } + + public List getMessagesByDirection(Direction direction) { + return switch (direction) { + case INPUT -> getInputs().stream().map(Input::getMessage).toList(); + case OUTPUT -> getOutputs().stream().map(Output::getMessage).toList(); + case FAULT -> getFaults().stream().map(Fault::getMessage).toList(); + }; + } + + public static abstract class OperationMessage extends WSDLElement { + public OperationMessage(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public Message getMessage() { + return ctx.definitions().findMessage(WSDLParserUtil.getLocalName(getAttribute("message"))) + .orElseThrow(() -> new WSDLParserException("Message not found: " + getAttribute("message"))); + } + } + + public static class Input extends OperationMessage { + public Input(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + } + + public static class Output extends OperationMessage { + public Output(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + } + + public static class Fault extends OperationMessage { + public Fault(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Part.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Part.java new file mode 100644 index 0000000000..addf8988f9 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Part.java @@ -0,0 +1,44 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.jetbrains.annotations.*; +import org.w3c.dom.*; + +import javax.xml.namespace.*; + +public class Part extends WSDLElement { + + public Part(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public QName getTypeQName() { + return getAttributeQName("type"); + } + + public QName getElementQName() { + return getAttributeQName("element"); + } + + private @Nullable QName getAttributeQName(String attribute) { + return resolveQName(getAttribute(attribute)); + } + + @Override + public String toString() { + return "Part [element=%s, type=%s]".formatted(getElementQName(), getTypeQName()); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Port.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Port.java new file mode 100644 index 0000000000..6cd78c36b3 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Port.java @@ -0,0 +1,40 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import static com.predic8.membrane.core.util.wsdl.parser.WSDLParserUtil.getLocalName; + +public class Port extends WSDLElement { + + public Port(WSDLParserContext ctx, Node node) { + super(ctx,node); + } + + public Address getAddress() { + return instantiateElements(element,"address",Address.class).getFirst(); + } + + public Binding getBinding() { + return ctx.definitions().getBindings().stream() + .filter(this::matchesTypeAttribute) + .findFirst().orElseThrow(() -> new WSDLParserException("No binding found for port: " + getName())); + } + + private boolean matchesTypeAttribute(Binding b) { + return b.getName().equals(getLocalName(getAttribute("binding"))); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/PortType.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/PortType.java new file mode 100644 index 0000000000..b89ffa9f46 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/PortType.java @@ -0,0 +1,30 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import java.util.*; + +public class PortType extends WSDLElement { + + public PortType(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public List getOperations() { + return instantiateWSDLChildren( "operation", Operation.class); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/ProtocolOperation.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/ProtocolOperation.java new file mode 100644 index 0000000000..6a536f6642 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/ProtocolOperation.java @@ -0,0 +1,16 @@ +package com.predic8.membrane.core.util.wsdl.parser; + +import org.w3c.dom.*; + +/** + * e.g., wsdl:binding/wsdl:operation/s11:operation + */ +public class ProtocolOperation extends WSDLElement { + public ProtocolOperation(WSDLParserContext ctx, Node node) { + super(ctx, node); + } + + public String getSoapAction() { + return getAttribute("soapAction"); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Service.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Service.java new file mode 100644 index 0000000000..db32c9bb4d --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/Service.java @@ -0,0 +1,30 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import java.util.*; + +public class Service extends WSDLElement { + + public Service(WSDLParserContext ctx, Node element) { + super(ctx,element); + } + + public List getPorts() { + return instantiateWSDLChildren( "port", Port.class); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLElement.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLElement.java new file mode 100644 index 0000000000..df3c035010 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLElement.java @@ -0,0 +1,134 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.jetbrains.annotations.*; +import org.w3c.dom.*; + +import javax.xml.namespace.*; +import java.util.*; +import java.util.function.*; + +import static com.predic8.membrane.annot.Constants.*; +import static org.w3c.dom.Node.*; + +public class WSDLElement { + + protected WSDLParserContext ctx; + protected final Element element; + + public WSDLElement(WSDLParserContext ctx, Node node) { + this.ctx = ctx; + if (!(node instanceof Element element)) { + throw new RuntimeException("Not an element: " + node.getClass()); + } + this.element = element; + } + + public String getName() { + return getAttribute("name"); + } + + public Element getDefinitions() { + return element.getOwnerDocument().getDocumentElement(); + } + + public Element getElement() { + return element; + } + + protected Optional instantiateChild(String name, Class clazz) { + var children = instantiateChildren(name, clazz); + return children.isEmpty() ? Optional.empty() : Optional.of(children.getFirst()); + } + + protected List instantiateChildren(String name, Class clazz) { + return instantiateElements(element,name,clazz); + } + + protected List instantiateElements(Node node, String name, Class clazz) { + return instantiateElementsInternal(node, name, clazz, WSDLElement::isElementWithName); + } + + protected List instantiateWSDLChildren(String name, Class clazz) { + return instantiateWSDLElements(element,name,clazz); + } + + protected List instantiateWSDLElements(Node node, String name, Class clazz) { + return instantiateElementsInternal(node, name, clazz, WSDLElement::isWSDLElementWithName); + } + + protected List instantiateXSDChildren(String name, Class clazz) { + return instantiateXSDElements(element,name,clazz); + } + + protected List instantiateXSDElements(Node node, String name, Class clazz) { + return instantiateElementsInternal(node, name, clazz, WSDLElement::isXSDElementWithName); + } + + protected List instantiateElementsInternal(Node node, String name, Class clazz, BiPredicate predicate) { + var result = new ArrayList(); + var children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + var child = children.item(i); + if (predicate.test(child, name)) { + try { + result.add(clazz.getConstructor(WSDLParserContext.class, Node.class).newInstance(ctx, child)); + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate " + clazz.getSimpleName() + " for element: " + name, e); + } + } + } + return result; + } + + protected static boolean isElementWithName(Node node, String name) { + return node.getNodeType() == ELEMENT_NODE && name.equals(node.getLocalName()); + } + + protected static boolean isWSDLElementWithName(Node node, String name) { + return isWSDLElement(node) && name.equals(node.getLocalName()); + } + + protected static boolean isXSDElementWithName(Node node, String name) { + return isXSDElement(node) && name.equals(node.getLocalName()); + } + + protected static boolean isWSDLElement(Node node) { + return node.getNodeType() == ELEMENT_NODE && WSDL11_NS.equals(node.getNamespaceURI()); + } + + protected static boolean isXSDElement(Node node) { + return node.getNodeType() == ELEMENT_NODE && XSD_NS.equals(node.getNamespaceURI()); + } + + protected boolean isWsdlSoap12Element() { + return WSDL_SOAP12_NS.equals(element.getNamespaceURI()); + } + + protected boolean isWsdlSoap11Element() { + return WSDL_SOAP11_NS.equals(element.getNamespaceURI()); + } + + protected @NotNull String getAttribute(String name) { + return element.getAttribute(name); + } + + public QName resolveQName(String value) { + if (value == null) + return null; + return WSDLParserUtil.resolveQName(value, element); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserContext.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserContext.java new file mode 100644 index 0000000000..ea27c94a21 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserContext.java @@ -0,0 +1,36 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import com.predic8.membrane.core.resolver.*; + +import java.util.*; + +public record WSDLParserContext(Definitions definitions, Resolver resolver, String basePath, + List visitedLocations) { + + public WSDLParserContext definitions(Definitions definitions) { + return new WSDLParserContext(definitions, resolver, basePath, visitedLocations); + } + + public WSDLParserContext basePath(String basePath) { + visitedLocations.add(basePath); + return new WSDLParserContext(definitions, resolver, basePath, visitedLocations); + } + + public WSDLParserContext resolver(Resolver resolver) { + return new WSDLParserContext(definitions, resolver, basePath, visitedLocations); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserException.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserException.java new file mode 100644 index 0000000000..6e6cc43df2 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserException.java @@ -0,0 +1,21 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +public class WSDLParserException extends RuntimeException { + public WSDLParserException(String message) { + super(message); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtil.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtil.java new file mode 100644 index 0000000000..beaca9c99e --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtil.java @@ -0,0 +1,56 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.w3c.dom.*; + +import javax.xml.namespace.*; +import javax.xml.parsers.*; +import java.io.*; + +public class WSDLParserUtil { + + public static Element parse(InputStream is) throws Exception { + var dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + return dbf.newDocumentBuilder().parse(is).getDocumentElement(); + } + + /** + * + * @param value QName with prefix like tns:MyPortType or just MyPortType + * @return + */ + public static String getLocalName(String value) { + var pos = value.indexOf(':'); + if (pos == -1) + return value; + return value.substring(pos+1); + } + + public static QName resolveQName(String value, Node context) { + if (value == null) + return null; + + if (!value.contains(":")) + return new QName(context.lookupNamespaceURI(null), value); + + String[] p = value.split(":"); + String prefix = p[0]; + String local = p[1]; + + return new QName(context.lookupNamespaceURI(prefix), local); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/AbstractIncludeImport.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/AbstractIncludeImport.java new file mode 100644 index 0000000000..722dc4aa30 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/AbstractIncludeImport.java @@ -0,0 +1,67 @@ +/* Copyright 2026 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.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.*; +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.jetbrains.annotations.*; +import org.w3c.dom.*; + +public abstract class AbstractIncludeImport extends WSDLElement { + + protected String schemaLocation; + protected Schema schema; + + public AbstractIncludeImport(WSDLParserContext ctx, Node node) { + super(ctx, node); + schemaLocation = getSchemaLocation(node); + schema = getSchema(ctx); + } + + public Schema getSchema() { + return schema; + } + + protected Schema getSchema(WSDLParserContext ctx) { + try { + var resolved = resolve(ctx); + + // Check if the schema has already been imported or included + if (ctx.visitedLocations().contains(resolved)) + return null; + + try (var is = ctx.resolver().resolve(resolved)) { + return new Schema(ctx.basePath(resolved), WSDLParserUtil.parse(is)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String resolve(WSDLParserContext ctx) { + return URIUtil.getNormalizedAbsolutePathOrUri(ResolverMap.combine(ctx.basePath(), schemaLocation)); + } + + public String getSchemaLocation() { + return schemaLocation; + } + + private static @NotNull String getSchemaLocation(Node node) { + if (node instanceof Element e) + return e.getAttribute("schemaLocation"); + return ""; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Import.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Import.java new file mode 100644 index 0000000000..68ba64cd2c --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Import.java @@ -0,0 +1,71 @@ +/* Copyright 2026 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.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.slf4j.*; +import org.w3c.dom.*; + +import java.util.*; + +public class Import extends AbstractIncludeImport { + + private static final Logger log = LoggerFactory.getLogger(Import.class); + + public Import(WSDLParserContext ctx, Node node) { + super(ctx, node); + + // Schema is null when it is already imported from somewhere else + if (schema == null) + return; + + if (!Objects.equals(schema.getTargetNamespace(), getNamespace())) { + throw new WSDLParserException("The namespace {%s} of the import does not match the targetNamespace of the imported schema {%s}.".formatted(getNamespace(), schema.getTargetNamespace())); + } + } + + /** + * Imports an import in a schema that is embedded in the WSDL definition. + * Function is typically called from Definitions.importEmbeddedSchemas(). + * @param definitions The WSL that contains the embedded schema. + */ + public void importEmbeddedSchema(Definitions definitions) { + if (hasSchemaLocation()) + return; + + log.debug("Importing embedded schema with namespace: {}", getNamespace()); + definitions.getEmbeddedSchema(getNamespace()).ifPresent(s -> schema = s); + } + + private boolean hasSchemaLocation() { + return schemaLocation != null && !schemaLocation.isEmpty(); + } + + @Override + protected Schema getSchema(WSDLParserContext ctx) { + if (schemaLocation == null || schemaLocation.isEmpty()) { + return null; + } + return super.getSchema(ctx); + } + + public void setSchema(Schema schema) { + this.schema = schema; + } + + public String getNamespace() { + return getAttribute("namespace"); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Include.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Include.java new file mode 100644 index 0000000000..6f193cae49 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Include.java @@ -0,0 +1,25 @@ +/* Copyright 2026 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.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.w3c.dom.*; + +public class Include extends AbstractIncludeImport { + + public Include(WSDLParserContext ctx, Node node) { + super(ctx,node); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Schema.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Schema.java new file mode 100644 index 0000000000..bffb3e3e41 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Schema.java @@ -0,0 +1,71 @@ +/* Copyright 2026 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.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.w3c.dom.*; + +import java.util.*; + +public class Schema extends WSDLElement { + + // These values are read repeatably so they are stored + private final List schemaElements; + private final List imports; + private final List includes; + + public Schema(WSDLParserContext ctx, Node node) { + super(ctx, node); + this.schemaElements = instantiateXSDChildren("element", SchemaElement.class); + imports = instantiateXSDChildren("import",Import.class); + includes = instantiateXSDChildren("include",Include.class); + includes.forEach(i -> include(i.getSchema())); + } + + public String getTargetNamespace() { + return getAttribute("targetNamespace"); + } + + public Element getSchemaElement() { + return element; + } + + public List getSchemaElements() { + return schemaElements; + } + + public List getImports() { + return imports; + } + + public List getIncludes() { + return includes; + } + + /** + * Take everything from the included schema and add it to the current schema. + * At the moment only elements are added. More is not needed. + * Later other features can be added as needed. + * + * @param includedSchema the schema whose elements are to be added to the current schema + */ + public void include(Schema includedSchema) { + if (includedSchema == null) + return; + + schemaElements.addAll(includedSchema.getSchemaElements()); + imports.addAll(includedSchema.getImports()); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/SchemaElement.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/SchemaElement.java new file mode 100644 index 0000000000..ec6d0f25df --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/SchemaElement.java @@ -0,0 +1,30 @@ +/* Copyright 2026 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.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.w3c.dom.*; + +public class SchemaElement extends WSDLElement { + + public SchemaElement(WSDLParserContext ctx, Node node) { + super(ctx,node); + } + + @Override + public String toString() { + return "SchemaElement{name=%s}".formatted(getName()); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Types.java b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Types.java new file mode 100644 index 0000000000..b391da9ef2 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/wsdl/parser/schema/Types.java @@ -0,0 +1,10 @@ +package com.predic8.membrane.core.util.wsdl.parser.schema; + +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.w3c.dom.*; + +public class Types extends WSDLElement { + public Types(WSDLParserContext ctx, Node node) { + super(ctx, node); + } +} 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 7feffa5d03..25f0639209 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 @@ -25,11 +25,9 @@ import java.net.*; import java.util.*; -import static com.predic8.membrane.annot.Constants.*; - @NotThreadSafe public class Relocator { - private static final Logger log = LoggerFactory.getLogger(Relocator.class.getName()); + private static final Logger log = LoggerFactory.getLogger(Relocator.class); private final XMLEventFactory fac = XMLEventFactory.newInstance(); @@ -42,8 +40,6 @@ public class Relocator { private final Map relocatingAttributes = new HashMap<>(); - private boolean wsdlFound; - public Relocator(OutputStreamWriter osw, String protocol, String host, int port, String contextPath, PathRewriter pathRewriter) throws Exception { this.writer = XMLOutputFactory.newInstance().createXMLEventWriter(osw); @@ -56,7 +52,7 @@ public Relocator(OutputStreamWriter osw, String protocol, String host, public static String getNewLocation(String addr, String protocol, String host, int port, String contextPath) { try { - URL oldURL = new URL(addr); + var oldURL = new URL(addr); if (port == -1) { return new URL(protocol, host, contextPath + oldURL.getFile()).toString(); } @@ -87,24 +83,12 @@ private void relocate(XMLEventReader parser) throws XMLStreamException { } private XMLEvent process(XMLEventReader parser) throws XMLStreamException { - XMLEvent event = parser.nextEvent(); + var event = parser.nextEvent(); if (!event.isStartElement()) return event; - if (getElementName(event).getNamespaceURI().equals(WSDL_SOAP11_NS) - || getElementName(event).getNamespaceURI().equals(WSDL_SOAP12_NS)) { - wsdlFound = true; - } - - return relocatingAttributes.entrySet().stream() - .filter(e -> shouldProcess(e, event)) - .findFirst() - .map(e -> replace(event, e.getValue())) - .orElse(event); - } - - private boolean shouldProcess(Map.Entry e, XMLEvent event) { - return getElementName(event).equals(e.getKey()); + var attr = relocatingAttributes.get(getElementName(event)); + return attr != null ? replace(event, attr) : event; } private QName getElementName(XMLEvent event) { @@ -112,16 +96,12 @@ private QName getElementName(XMLEvent event) { } private XMLEvent replace(XMLEvent event, String attribute) { - StartElement start = event.asStartElement(); + var start = event.asStartElement(); return fac.createStartElement(start.getName(), new ReplaceIterator(fac, attribute, start.getAttributes()), start.getNamespaces()); } - public boolean isWsdlFound() { - return wsdlFound; - } - public Map getRelocatingAttributes() { return relocatingAttributes; } @@ -147,9 +127,9 @@ public boolean hasNext() { } public Attribute next() { - Attribute atr = attrs.next(); + var atr = attrs.next(); if (atr.getName().equals(new QName(replace))) { - String value = atr.getValue(); + var value = atr.getValue(); if (pathRewriter != null) { value = pathRewriter.rewrite(value); if (value.startsWith("http")) diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPFaultTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPFaultTest.java index 0d95a7f115..e0e399295a 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPFaultTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPFaultTest.java @@ -36,7 +36,7 @@ public class SOAPFaultTest { @Test public void testValidateFaults() { - ValidatorInterceptor i = createValidatorInterceptor(false); + var i = createValidatorInterceptor(false); Exchange exc = createFaultExchange(); assertEquals(ABORT, i.handleResponse(exc)); assertContainsNot("secret", exc.getResponse().toString()); diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPMessageValidatorInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPMessageValidatorInterceptorTest.java index 310d632363..0afe29b10e 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPMessageValidatorInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPMessageValidatorInterceptorTest.java @@ -128,7 +128,7 @@ void testHandleRequestInvalidEmailMessageDoubleRequestElement() throws Exception @Test void handleRequestInvalidEmailMessageUnknownElement() throws Exception { - Exchange exc = post("http://ws.xwebservices.com") + var exc = post("http://ws.xwebservices.com") .body(getContent("/validation/invalidEmail3.xml")) .buildExchange(); assertEquals(ABORT, createValidatorInterceptor(E_MAIL_SERVICE_WSDL).handleRequest(exc)); @@ -149,9 +149,10 @@ void inlineSchemaWithAnyType() throws Exception { var body = exc.getResponse().getBodyAsStringDecoded(); assertTrue(body.contains(WSDL_MESSAGE_VALIDATION_FAILED)); - assertTrue(body.contains("cvc-elt.1.a")); - assertTrue(body.contains("line")); - assertTrue(body.contains("column")); + assertTrue(body.contains("ValidateEmailRequest")); + assertTrue(body.contains("is not a valid request element")); + assertTrue(body.contains("http://www.examples.com/wsdl/HelloService.wsdl")); + assertTrue(body.contains("sayHello")); } private String getContent(String fileName) throws Exception { diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPUtilTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPUtilTest.java index 5362b48e4c..5fad03c866 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/SOAPUtilTest.java @@ -31,77 +31,100 @@ public class SOAPUtilTest { - private final static String TB_NS = "http://thomas-bayer.com/blz/"; - private final static String MEMBRANE_NS = "http://membrane-api.io/"; - - @Test - void faultCheckSpecExample() throws Exception { - assertTrue(SOAPUtil.analyseSOAPMessage( new XOPReconstitutor(), getMessage("src/test/resources/wsdlValidator/soapFaultFromSpec.xml")).isFault()); - } - - @Test - void faultCustom() throws Exception { - assertTrue(SOAPUtil.analyseSOAPMessage( new XOPReconstitutor(), getMessage("src/test/resources/wsdlValidator/soapFaultCustom.xml")).isFault()); - } - - @Test - void analyseXML() { - SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage( new XOPReconstitutor(), getMessageFromString("")); - assertFalse(result.isSOAP()); - assertFalse(result.isFault()); - } - - @Test - void analyseSOAP11() { - SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage( new XOPReconstitutor(), getMessageFromString(""" - - - - 66762332 - - - - """)); - assertTrue(result.isSOAP()); - assertFalse(result.isFault()); - assertEquals(SOAP11, result.version()); - assertEquals(new QName(TB_NS,"getBank"), result.soapElement()); - } - - @Test - void analyseSOAP12() { - SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage( new XOPReconstitutor(), getMessageFromString(""" - - - - - - """)); - assertTrue(result.isSOAP()); - assertFalse(result.isFault()); - assertEquals(SOAP12, result.version()); - assertEquals(new QName(MEMBRANE_NS,"Bar"), result.soapElement()); - } - - @Test - void analyseFault11() { - SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage( new XOPReconstitutor(), getMessageFromString(""" - - - - - - """)); - assertTrue(result.isSOAP()); - assertTrue(result.isFault()); - assertEquals(SOAP11, result.version()); - } - - private Message getMessageFromString(String body) { - return ok().contentType(TEXT_XML).body(body).build(); - } - - private Message getMessage(String path) throws Exception { - return ok().contentType(TEXT_XML).body(new FileInputStream(path), true).build(); - } + private final static String TB_NS = "http://thomas-bayer.com/blz/"; + private final static String MEMBRANE_NS = "http://membrane-api.io/"; + + @Test + void faultCheckSpecExample() throws Exception { + assertTrue(SOAPUtil.analyseSOAPMessage(new XOPReconstitutor(), getMessage("src/test/resources/wsdlValidator/soapFaultFromSpec.xml")).isFault()); + } + + @Test + void faultCustom() throws Exception { + assertTrue(SOAPUtil.analyseSOAPMessage(new XOPReconstitutor(), getMessage("src/test/resources/wsdlValidator/soapFaultCustom.xml")).isFault()); + } + + @Test + void analyseXML() { + SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage(new XOPReconstitutor(), getMessageFromString("")); + assertFalse(result.isSOAP()); + assertFalse(result.isFault()); + } + + @Test + void analyseSOAP11() { + SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage(new XOPReconstitutor(), getMessageFromString(""" + + + + 66762332 + + + + """)); + assertTrue(result.isSOAP()); + assertFalse(result.isFault()); + assertEquals(SOAP11, result.version()); + assertEquals(new QName(TB_NS, "getBank"), result.soapElement()); + } + + @Test + void analyseSOAP12() { + SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage(new XOPReconstitutor(), getMessageFromString(""" + + + + + + """)); + assertTrue(result.isSOAP()); + assertFalse(result.isFault()); + assertEquals(SOAP12, result.version()); + assertEquals(new QName(MEMBRANE_NS, "Bar"), result.soapElement()); + } + + @Test + void analyseFault11() { + SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage(new XOPReconstitutor(), getMessageFromString(""" + + + + + + """)); + assertTrue(result.isSOAP()); + assertTrue(result.isFault()); + assertEquals(SOAP11, result.version()); + } + + /** + * Fault is not namespace prefixed. => Ok + */ + @Test + void analyseFault11DifferentNamespace() { + SOAPUtil.SOAPAnalysisResult result = analyseSOAPMessage(new XOPReconstitutor(), getMessageFromString(""" + + + + Client + WSDL message validation failed + + Not a valid SOAP message. + + + + + """)); + assertTrue(result.isSOAP()); + assertTrue(result.isFault()); + assertEquals(SOAP11, result.version()); + } + + private Message getMessageFromString(String body) { + return ok().contentType(TEXT_XML).body(body).build(); + } + + private Message getMessage(String path) throws Exception { + return ok().contentType(TEXT_XML).body(new FileInputStream(path), true).build(); + } } diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractorTest.java new file mode 100644 index 0000000000..128f3c947e --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLMessageElementExtractorTest.java @@ -0,0 +1,72 @@ +/* Copyright 2026 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.core.interceptor.schemavalidation; + +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.wsdl.parser.*; +import org.jetbrains.annotations.*; +import org.junit.jupiter.api.*; + +import javax.xml.namespace.*; + +import static com.predic8.membrane.core.interceptor.schemavalidation.WSDLMessageElementExtractor.*; +import static org.junit.jupiter.api.Assertions.*; + +class WSDLMessageElementExtractorTest { + + private static final String CITIES_NS = "https://predic8.de/cities"; + private static final QName GET_CITY_QNAME = new QName(CITIES_NS, "getCity"); + private static final QName GET_CITYRESPONSE_QNAME = new QName(CITIES_NS, "getCityResponse"); + + private static final String XMAIL_NS = "urn:ws-xwebservices-com:XWebEmailValidation:EmailValidation:v2:Messages"; + private static final QName GET_EMAIL_QNAME = new QName(XMAIL_NS, "ValidateEmailRequest"); + private static final QName GET_EMAILRESPONSE_QNAME = new QName(XMAIL_NS, "ValidateEmailResponse"); + + @Test + void extract() throws Exception { + var requestElements = getPossibleRequestElements(getDefinitions("classpath:/ws/cities.wsdl"), null); + + assertEquals(1, requestElements.size()); + assertTrue(requestElements.contains(GET_CITY_QNAME)); + + var responseElements = getPossibleResponseElements(getDefinitions("classpath:/ws/cities.wsdl"), null); + assertEquals(1, responseElements.size()); + assertTrue(responseElements.contains(GET_CITYRESPONSE_QNAME)); + + } + + @Test + void eMailServiceWSDL() throws Exception { + var requestElements = getPossibleRequestElements(getDefinitions("classpath:/validation/XWebEmailValidation.wsdl.xml"), null); + assertEquals(1, requestElements.size()); + assertTrue(requestElements.contains(GET_EMAIL_QNAME)); + + var responseElements = getPossibleResponseElements(getDefinitions("classpath:/validation/XWebEmailValidation.wsdl.xml"), null); + assertEquals(1, responseElements.size()); + assertTrue(responseElements.contains(GET_EMAILRESPONSE_QNAME)); + } + + @Test + void rpcStyle() throws Exception { + var requestElements = getPossibleRequestElements(getDefinitions("classpath:/validation/inline-anytype.wsdl"), "Hello_Service"); + assertEquals(1, requestElements.size()); + assertTrue(requestElements.contains(new QName("http://www.examples.com/wsdl/HelloService.wsdl", "sayHello"))); + } + + private static @NotNull Definitions getDefinitions(String location) throws Exception { + return Definitions.parse(new ResolverMap(), location); + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractorTest.java new file mode 100644 index 0000000000..c3228f28d8 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLSchemaExtractorTest.java @@ -0,0 +1,53 @@ +/* Copyright 2026 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.core.interceptor.schemavalidation; + +import com.predic8.membrane.annot.*; +import org.junit.jupiter.api.*; +import org.w3c.dom.*; +import org.xml.sax.*; + +import javax.xml.parsers.*; +import javax.xml.transform.dom.*; +import java.io.*; + +import static com.predic8.membrane.annot.Constants.WSDL_SOAP11_NS; +import static com.predic8.membrane.annot.Constants.XSD_NS; +import static com.predic8.membrane.core.interceptor.schemavalidation.WSDLSchemaExtractor.*; +import static org.junit.jupiter.api.Assertions.*; + +class WSDLSchemaExtractorTest { + + @Test + void extractWithCorrectNamespaceFromWSDLDefinitionElement() throws Exception { + + var schemas = getSchemas(parse( this.getClass().getResourceAsStream("/ws/cities.wsdl")) + .getDocumentElement()); + assertEquals(1, schemas.size()); + + var schema = schemas.getFirst().getDocumentElement(); + assertEquals(XSD_NS , schema.getNamespaceURI()); + assertEquals("schema", schema.getLocalName()); + + assertEquals(XSD_NS, schema.getAttribute("xmlns:xsd")); + assertEquals(WSDL_SOAP11_NS, schema.getAttribute("xmlns:s")); + } + + private static Document parse(InputStream wsdl) throws Exception { + var fac = DocumentBuilderFactory.newInstance(); + fac.setNamespaceAware(true); + return fac.newDocumentBuilder().parse(wsdl); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidatorTest.java index 034f645970..11a34274bd 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidatorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/WSDLValidatorTest.java @@ -39,7 +39,7 @@ public class WSDLValidatorTest { @Test void invalidRequestElement() throws Exception { - Exchange exc = getRequestExchange(soap11(""" + var exc = getRequestExchange(soap11(""" """)); @@ -108,8 +108,7 @@ void twoServicesElementOfProperService() throws Exception { @Test void twoServicesElementOfWrongService() throws Exception { - - Exchange exc = getRequestExchange(soap11(""" + var exc = getRequestExchange(soap11(""" Paris """)); @@ -134,12 +133,15 @@ void validateFaultsAndFail() throws Exception { @Test void skipFaults() throws Exception { - Exchange exc = getResponseExchange(soap11(""" + var exc = getResponseExchange(soap11(""" """)); - assertEquals(CONTINUE, createValidator(CITIES_WSDL, null, true).validateMessage(exc, RESPONSE)); + + Outcome actual = createValidator(CITIES_WSDL, null, true).validateMessage(exc, RESPONSE); dumpResonseBody(exc); + assertEquals(CONTINUE, actual); + } @Test @@ -148,14 +150,14 @@ void multiplePortsSoap11() throws Exception { Paris """)); - Outcome outcome = createValidator(MULTIPLE_PORTS_WSDL, "Service", false).validateMessage(exc, REQUEST); + Outcome outcome = createValidator(MULTIPLE_PORTS_WSDL, "Service", true).validateMessage(exc, REQUEST); dumpResonseBody(exc); assertEquals(CONTINUE, outcome); } @Test void abstractWsdl() throws Exception { - Exchange exc = getRequestExchange(soap11(""" + var exc = getRequestExchange(soap11(""" Paris """)); @@ -168,11 +170,11 @@ void abstractWsdl() throws Exception { @Test void abstractWsdlNoReferencedRequestElement() throws Exception { - Exchange exc = getRequestExchange(soap11(""" + var exc = getRequestExchange(soap11(""" 7 """)); - Outcome outcome = createValidator(ABSTRACT_SERVICE_NO_BINDING_WSDL, null, false).validateMessage(exc, REQUEST); + var outcome = createValidator(ABSTRACT_SERVICE_NO_BINDING_WSDL, null, false).validateMessage(exc, REQUEST); dumpResonseBody(exc); assertEquals(ABORT, outcome); assertTrue(exc.getResponse().getBodyAsStringDecoded().contains("is not a valid request element")); @@ -212,7 +214,7 @@ private static Exchange getResponseExchange(String body) { } private static WSDLValidator createValidator(String location, String serviceName, boolean skipFaults) { - WSDLValidator validator = new WSDLValidator(new ResolverMap(), location, serviceName, (msg, exc) -> log.info("Validation failure: {}", msg), skipFaults); + var validator = new WSDLValidator(new ResolverMap(), location, serviceName, (msg, exc) -> log.info("Validation failure: {}", msg), skipFaults); validator.init(); return validator; } 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 ee9f99911e..2198e57c5f 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 @@ -85,6 +85,14 @@ void parseWSDLWithMultipleServicesForAGivenServiceA() throws Exception { router.start(); } + @Test + void abstractWSDL() throws Exception { + proxy.setWsdl("classpath:/ws/abstract-service-no-binding.wsdl"); + router.add(proxy); + assertThrows(ConfigurationException.class, + () -> router.start()); + } + @Test void parseWSDLWithMultipleServicesForAGivenServiceB() throws Exception { proxy.setServiceName("CityServiceB"); @@ -108,7 +116,7 @@ void parseWSDLWithMultipleServicesForAWrongService() { proxy.setServiceName("WrongService"); proxy.setWsdl("classpath:/ws/cities-2-services.wsdl"); - assertThrows(IllegalArgumentException.class, () -> { + assertThrows(ConfigurationException.class, () -> { router.add(proxy); router.start(); }); diff --git a/core/src/test/java/com/predic8/membrane/core/resolver/ResolverTest.java b/core/src/test/java/com/predic8/membrane/core/resolver/ResolverTest.java index c2dca5d940..41cf1d20ab 100644 --- a/core/src/test/java/com/predic8/membrane/core/resolver/ResolverTest.java +++ b/core/src/test/java/com/predic8/membrane/core/resolver/ResolverTest.java @@ -20,8 +20,6 @@ import com.predic8.membrane.core.proxies.*; import com.predic8.membrane.core.router.*; import com.predic8.membrane.core.util.*; -import com.predic8.schema.*; -import com.predic8.wsdl.*; import org.junit.jupiter.api.*; import org.junit.jupiter.params.*; import org.junit.jupiter.params.provider.*; @@ -143,32 +141,13 @@ public void testMembraneServiceProxyCombine(BasisUrlType basisUrlType) throws IO assertNotNull(resolverMap.resolve(wsdlLocation)); for (String relUrl : new String[]{"1.xsd", "./1.xsd", "../resolver/1.xsd", "http://localhost:3029/resolver/1.xsd"}) { try { - assertNotNull(resolverMap.resolve(ResolverMap.combine(wsdlLocation, relUrl))); + assertNotNull(resolverMap.resolve(ResolverMap.combine(URIFactory.DEFAULT_URI_FACTORY, wsdlLocation, relUrl))); } catch (Exception e) { throw new RuntimeException("Error during combine(\"" + wsdlLocation + "\", \"" + relUrl + "\"):", e); } } } - @ParameterizedTest - @MethodSource("getConfigurations") - public void testMembraneSoaModel(BasisUrlType basisUrlType) { - if (hit = !setupLocations(basisUrlType)) - return; - - try { - WSDLParserContext ctx = new WSDLParserContext(); - ctx.setInput(wsdlLocation); - WSDLParser wsdlParser = new WSDLParser(); - wsdlParser.setResourceResolver(resolverMap.toExternalResolver().toExternalResolver()); - Definitions definitions = wsdlParser.parse(ctx); - for (Schema schema : definitions.getSchemas()) - schema.getElements(); // trigger lazy-loading - } catch (Exception e) { - throw new RuntimeException("wsdlLocation = " + xsdLocation, e); - } - } - @AfterEach public void postpare() { // since a.wsdl and 2.xsd reference a HTTP resource, it should get loaded diff --git a/core/src/test/java/com/predic8/membrane/core/util/URIUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/URIUtilTest.java index 048f372a6f..2d08c25a27 100644 --- a/core/src/test/java/com/predic8/membrane/core/util/URIUtilTest.java +++ b/core/src/test/java/com/predic8/membrane/core/util/URIUtilTest.java @@ -14,15 +14,18 @@ package com.predic8.membrane.core.util; import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.*; import java.io.*; import java.net.*; import java.net.URI; +import java.nio.file.*; import java.util.function.*; import static com.predic8.membrane.core.util.URIUtil.*; import static java.util.Optional.*; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.condition.OS.*; /** * Unfortunately the file: protocol is @@ -36,7 +39,7 @@ public class URIUtilTest { /** * Used to keep the tests and to try different URI or URIUtils implementations */ - static Function converter; + static Function converter; @BeforeAll static void setup() { @@ -86,17 +89,17 @@ void localhost() { } @Test - void winColon() { + void winColon() { assertEquals("C:\\foo", converter.apply("file://C:/foo")); } @Test - void winSlash() { + void winSlash() { assertEquals("C:\\foo", converter.apply("file://C/foo")); } @Test - void winSlashThree() { + void winSlashThree() { assertEquals("C:\\foo", pathFromFileURI("file:///C/foo")); } @@ -152,6 +155,7 @@ void backslashesWindows() { assertEquals("\\\\", URIUtil.slashToBackslash("//")); assertEquals("a\\b\\c", URIUtil.slashToBackslash("a/b/c")); } + @Test void driveLetterAndSlash() { assertEquals("b", removeDriveLetterAndSlash("a/b")); @@ -179,8 +183,8 @@ void normalizeSingleDot() { @Test void toFileURIStringTest() throws URISyntaxException { - assertEquals(wl("file:/C:/swig/jig","file:/swig/jig"), FileUtil.toFileURIString(new File("/swig/jig"))); - assertEquals(wl("file:/C:/jag%20sag/runt","file:/jag%20sag/runt"), FileUtil.toFileURIString(new File("/jag sag/runt"))); + assertEquals(wl("file:/C:/swig/jig", "file:/swig/jig"), FileUtil.toFileURIString(new File("/swig/jig"))); + assertEquals(wl("file:/C:/jag%20sag/runt", "file:/jag%20sag/runt"), FileUtil.toFileURIString(new File("/jag sag/runt"))); } String wl(String windows, String linux) { @@ -194,7 +198,7 @@ void toFileURIStringSpaceTest() throws URISyntaxException { assertEquals(wl( "file:/C:/chip%20clip", "file:/chip%20clip" - ), FileUtil.toFileURIString(new File("/chip clip"))); + ), FileUtil.toFileURIString(new File("/chip clip"))); } @Test @@ -206,4 +210,106 @@ void convertPath2FileURITest() throws URISyntaxException { assertEquals(new URI("file:///foo"), convertPath2FileURI("file:///foo")); assertEquals(new URI("file:/c:/foo/boo"), convertPath2FileURI("c:\\foo\\boo")); } + + @Nested + class normalize { + @Test + void throwsOnNull() { + assertThrows(IllegalArgumentException.class, () -> getNormalizedAbsolutePathOrUri(null)); + } + + + @Test + void throwsOnEmpty() { + assertThrows(IllegalArgumentException.class, () -> getNormalizedAbsolutePathOrUri("")); + } + + @Test + void normalizesRelativeFilesystemPath() { + var input = "foo/../bar/test.txt"; + var expected = Path.of(input).toAbsolutePath().normalize().toString(); + assertEquals(expected, getNormalizedAbsolutePathOrUri(input)); + } + + @Test + void normalizesAbsoluteFilesystemPath() { + var input = Path.of("foo", "..", "bar").toAbsolutePath().toString(); + var expected = Path.of(input).toAbsolutePath().normalize().toString(); + assertEquals(expected, getNormalizedAbsolutePathOrUri(input)); + } + + @Test + void normalizesFileUri() { + assertEquals("file:/test.xml", getNormalizedAbsolutePathOrUri("file:///tmp/../test.xml")); + } + + @Test + void normalizesHttpUri() { + assertEquals("http://example.com/b/wsdl.xsd", getNormalizedAbsolutePathOrUri("http://example.com/a/../b/wsdl.xsd")); + } + + @Test + void normalizesHttpUriWithQuery() { + assertEquals("http://example.com/b/wsdl.xsd?a=a/../b", getNormalizedAbsolutePathOrUri("http://example.com/a/../b/wsdl.xsd?a=a/../b")); + } + + + @Test + void classpathUri() { + // The '..' within the path portion normalizes correctly. + assertEquals("classpath://authority/xsd/test.xsd", getNormalizedAbsolutePathOrUri("classpath://authority/schema/../xsd/test.xsd")); + } + + @Test + void keepsClasspathUri() { + // The '..' at the start of the path (after authority) cannot traverse above the authority, so it's preserved. + assertEquals("classpath://authority/../xsd/test.xsd", getNormalizedAbsolutePathOrUri("classpath://authority/../xsd/test.xsd")); + } + + @Test + void normalizesUnixLikePath() { + var input = "/tmp/../var/data.xsd"; + var expected = Path.of(input).toAbsolutePath().normalize().toString(); + assertEquals(expected, getNormalizedAbsolutePathOrUri(input)); + } + + @Test + @EnabledOnOs(WINDOWS) + void handlesWindowsDriveLetterPath() { + var input = "C:\\temp\\..\\data\\test.xsd"; + var expected = Path.of(input).toAbsolutePath().normalize().toString(); + assertEquals(expected, getNormalizedAbsolutePathOrUri(input)); + } + + @Test + @EnabledOnOs(WINDOWS) + void handlesWindowsDriveRelativePath() { + var input = "C:temp\\..\\data\\test.xsd"; + var expected = Path.of(input).toAbsolutePath().normalize().toString(); + assertEquals(expected, getNormalizedAbsolutePathOrUri(input)); + } + + @Test + void keepsRelativeUriWithQuery() { + assertEquals("schema.xsd?version=1", getNormalizedAbsolutePathOrUri("schema.xsd?version=1") + ); + } + + @Test + void keepsRelativeUriWithFragment() { + assertEquals("../types.xsd#frag", getNormalizedAbsolutePathOrUri("../types.xsd#frag") + ); + } + + @Test + void keepsRelativeUriWithQueryAndFragment() { + assertEquals("../types.xsd?version=1#frag", getNormalizedAbsolutePathOrUri("../types.xsd?version=1#frag") + ); + } + + @Test + void keepsSchemeRelativeReference() { + assertEquals("//example.com/schema.xsd?version=1", getNormalizedAbsolutePathOrUri("//example.com/schema.xsd?version=1")); + } + } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/WSDLUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/WSDLUtilTest.java deleted file mode 100644 index 6fc4858459..0000000000 --- a/core/src/test/java/com/predic8/membrane/core/util/WSDLUtilTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* Copyright 2024 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.core.util; - -import com.predic8.wsdl.*; -import org.jetbrains.annotations.*; -import org.junit.jupiter.api.*; - -import javax.xml.namespace.*; -import java.util.*; - -import static com.predic8.membrane.core.util.WSDLUtil.*; -import static com.predic8.membrane.core.util.WSDLUtil.Direction.*; -import static org.junit.jupiter.api.Assertions.*; - -public class WSDLUtilTest { - - public static final String PREDIC_NS = "https://predic8.de/"; - - static Definitions definitions; - - @BeforeAll - static void beforeAll() { - WSDLParserContext ctx = new WSDLParserContext(); - ctx.setInput("src/test/resources/ws/two-separated-services.wsdl"); - WSDLParser wsdlParser = new WSDLParser(); - definitions = wsdlParser.parse(ctx); - } - - @Test - void getServiceA() { - testGetServiceByName("ServiceA"); - } - - @Test - void getServiceB() { - testGetServiceByName("ServiceB"); - } - - @Test - void getNonExistingService() { - assertThrows(RuntimeException.class, () -> getService(definitions, "not there")); - } - - private static void testGetServiceByName(String ServiceA) { - Service service = getService(definitions, ServiceA); - assertNotNull(service); - assertEquals(ServiceA, service.getName()); - } - - @Test - void getPossibleSOAPElementsARequest() { - Set el = getSoapElements("ServiceA",REQUEST); - assertNotNull(el); - assertEquals(1,el.size()); - assertTrue(el.contains(new QName(PREDIC_NS,"a"))); - } - - @Test - void getPossibleSOAPElementsAResponse() { - Set el = getSoapElements("ServiceA", RESPONSE); - assertNotNull(el); - assertEquals(1,el.size()); - assertTrue(el.contains(new QName(PREDIC_NS,"aResponse"))); - } - - @Test - void getPossibleSOAPElementsBRequest() { - Set el = getSoapElements("ServiceB", REQUEST); - assertNotNull(el); - assertEquals(1,el.size()); - assertTrue( el.contains(new QName(PREDIC_NS,"b"))); - } - - private static @NotNull Set getSoapElements(String serviceName, Direction direction) { - return getPossibleSOAPElements(getService(definitions, serviceName), direction); - } -} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLParserTest.java b/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLParserTest.java new file mode 100644 index 0000000000..b3242a6a73 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/util/soap/WSDLParserTest.java @@ -0,0 +1,197 @@ +/* Copyright 2026 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.core.util.soap; + +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.wsdl.parser.*; +import com.predic8.membrane.core.util.wsdl.parser.schema.*; +import org.junit.jupiter.api.*; + +import java.util.*; + +import static com.predic8.membrane.core.util.wsdl.parser.Binding.Style.*; +import static org.junit.jupiter.api.Assertions.*; + +class WSDLParserTest { + + @Test + void simpleSchema() throws Exception { + var definitions = Definitions.parse(new ResolverMap(), "classpath:/ws/cities.wsdl"); + assertEquals("cities", definitions.getName()); + assertEquals(1, definitions.getSchemas().size()); + var schema = definitions.getSchemas().getFirst(); + assertEquals("https://predic8.de/cities", schema.getTargetNamespace()); + var schemaElements = schema.getSchemaElements(); + assertEquals(2, schemaElements.size()); + var schemaElementNames = schemaElements.stream().map(SchemaElement::getName).toList(); + assertEquals(List.of("getCity", "getCityResponse"), schemaElementNames); + + var services = definitions.getServices(); + assertEquals(1, services.size()); + var service = services.getFirst(); + assertEquals("CityService", service.getName()); + var ports = service.getPorts(); + assertEquals(1, ports.size()); + var port = ports.getFirst(); + assertEquals("CityPort", port.getName()); + var address = port.getAddress(); + assertEquals("http://localhost:2001/services/cities", address.getLocation()); + var binding = port.getBinding(); + assertEquals("CitySoapBinding", binding.getName()); + assertEquals(DOCUMENT, binding.getStyle()); + + assertEquals("https://predic8.de/cities", binding.getBindingOperation("getCity").getSoapAction()); + + + var portType = binding.getPortType(); + assertEquals("CityPort", portType.getName()); + var operations = portType.getOperations(); + assertEquals(1, operations.size()); + var getCityOperation = operations.getFirst(); + var inputs = getCityOperation.getInputs(); + assertEquals(1, inputs.size()); + var getCityInput = inputs.getFirst(); + var getCityPart = getCityInput.getMessage().getPart(); + assertEquals("getCity", getCityPart.getName()); + assertEquals("getCity", getCityPart.getElementQName().getLocalPart()); + assertEquals("https://predic8.de/cities", getCityPart.getElementQName().getNamespaceURI()); + + assertEquals(1, definitions.getBindings().size()); + var binding1 = definitions.getBindings().getFirst(); + assertEquals("CitySoapBinding", binding1.getName()); + assertEquals(DOCUMENT, binding1.getStyle()); + + assertEquals(2, definitions.getMessages().size()); + assertEquals("City", definitions.getMessages().getFirst().getName()); + assertEquals("CityResponse", definitions.getMessages().getLast().getName()); + } + + @Test + void includeImport() throws Exception { + var dn = Definitions.parse(new ResolverMap(), "classpath://ws/include/include.wsdl"); + assertEquals("", dn.getName()); // No name is set + assertEquals(1, dn.getSchemas().size()); + var embedded = dn.getSchemas().getFirst(); + assertEquals("http://example.com/test", embedded.getTargetNamespace()); + var schemaElements = embedded.getSchemaElements(); + assertEquals(3, schemaElements.size()); + var schemaElementNames = schemaElements.stream().map(SchemaElement::getName).toList(); + assertEquals(List.of("test", "testResponse", "du"), schemaElementNames); + + var includeMessageStructureTypes = embedded.getIncludes().getFirst(); + assertEquals("xsd/inc/MessageStructureTypes2.xsd", includeMessageStructureTypes.getSchemaLocation()); + + assertEquals(1, embedded.getImports().size()); + var imported = embedded.getImports().getFirst(); + assertEquals("http://example.com/test/data-types", imported.getNamespace()); + var importedSchemaElements = imported.getSchema().getSchemaElements(); + assertEquals(2, importedSchemaElements.size()); + var importedSchemaElementNames = importedSchemaElements.stream().map(SchemaElement::getName).toList(); + assertEquals(List.of("B5", "B6"), importedSchemaElementNames); + + } + + @Test + void abstractWSDL() throws Exception { + assertEquals(1, Definitions.parse(new ResolverMap(), "classpath:/ws/abstract-service-no-binding.wsdl").getPortTypes().size()); + } + + @Test + void fault() throws Exception { + var dn = Definitions.parse(new ResolverMap(), "classpath:/ws/calculator-fault.wsdl"); + var pt = dn.getPortTypes().getFirst(); + var fault = pt.getOperations().getFirst().getFaults().getFirst(); + assertNotNull(fault); + assertEquals("DivideByZeroFault", fault.getMessage().getPart().getElementQName().getLocalPart()); + } + + @Test + void rpcStyle() throws Exception { + var dn = Definitions.parse(new ResolverMap(), "classpath:/validation/inline-anytype.wsdl"); + assertEquals(RPC, dn.getBindings().getFirst().getStyle()); + } + + @Test + void soap12() throws Exception { + var definitions = Definitions.parse(new ResolverMap(), "classpath:/ws/hello-soap12.wsdl"); + + assertEquals("HelloService", definitions.getName()); + + assertEquals(1, definitions.getSchemas().size()); + var schema = definitions.getSchemas().getFirst(); + assertEquals("http://example.com/hello", schema.getTargetNamespace()); + + var schemaElements = schema.getSchemaElements(); + assertEquals(2, schemaElements.size()); + var schemaElementNames = schemaElements.stream().map(SchemaElement::getName).toList(); + assertEquals(List.of("sayHello", "sayHelloResponse"), schemaElementNames); + + var services = definitions.getServices(); + assertEquals(1, services.size()); + var service = services.getFirst(); + assertEquals("HelloService", service.getName()); + + var ports = service.getPorts(); + assertEquals(1, ports.size()); + var port = ports.getFirst(); + assertEquals("HelloPort", port.getName()); + + var address = port.getAddress(); + assertEquals("http://example.com/hello", address.getLocation()); + + var binding = port.getBinding(); + assertEquals("HelloBinding", binding.getName()); + assertEquals(DOCUMENT, binding.getStyle()); + + assertEquals("sayHello", binding.getBindingOperation("sayHello").getSoapAction()); + + var portType = binding.getPortType(); + assertEquals("HelloPortType", portType.getName()); + + var operations = portType.getOperations(); + assertEquals(1, operations.size()); + + var operation = operations.getFirst(); + assertEquals("sayHello", operation.getName()); + + var inputs = operation.getInputs(); + assertEquals(1, inputs.size()); + + var input = inputs.getFirst(); + var part = input.getMessage().getPart(); + assertEquals("parameters", part.getName()); + assertEquals("sayHello", part.getElementQName().getLocalPart()); + assertEquals("http://example.com/hello", part.getElementQName().getNamespaceURI()); + + var outputs = operation.getOutputs(); + assertEquals(1, outputs.size()); + + var output = outputs.getFirst(); + var outputPart = output.getMessage().getPart(); + assertEquals("parameters", outputPart.getName()); + assertEquals("sayHelloResponse", outputPart.getElementQName().getLocalPart()); + assertEquals("http://example.com/hello", outputPart.getElementQName().getNamespaceURI()); + + assertEquals(1, definitions.getBindings().size()); + var binding1 = definitions.getBindings().getFirst(); + assertEquals("HelloBinding", binding1.getName()); + assertEquals(DOCUMENT, binding1.getStyle()); + + assertEquals(2, definitions.getMessages().size()); + assertEquals("SayHelloRequest", definitions.getMessages().getFirst().getName()); + assertEquals("SayHelloResponse", definitions.getMessages().getLast().getName()); + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/OperationTest.java b/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/OperationTest.java new file mode 100644 index 0000000000..36d90d1c08 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/OperationTest.java @@ -0,0 +1,31 @@ +/* Copyright 2026 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.core.util.wsdl.parser; + +import org.junit.jupiter.api.*; + +import static com.predic8.membrane.core.util.wsdl.parser.Operation.Direction.INPUT; +import static com.predic8.membrane.core.util.wsdl.parser.Operation.Direction.OUTPUT; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OperationTest { + + @Test + void direction() { + assertTrue(INPUT.matches("input")); + assertTrue(OUTPUT.matches("OutPut")); + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtilTest.java new file mode 100644 index 0000000000..a0b484d4fe --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/util/wsdl/parser/WSDLParserUtilTest.java @@ -0,0 +1,15 @@ +package com.predic8.membrane.core.util.wsdl.parser; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +class WSDLParserUtilTest { + + @Test + void typeName() { + assertEquals("myPortType", WSDLParserUtil.getLocalName("myPortType")); + assertEquals("myPortType", WSDLParserUtil.getLocalName("tns:myPortType")); + } + +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/ws/WSDLIncludeImportTest.java b/core/src/test/java/com/predic8/membrane/core/ws/WSDLIncludeImportTest.java new file mode 100644 index 0000000000..260043161f --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/ws/WSDLIncludeImportTest.java @@ -0,0 +1,116 @@ +/* Copyright 2026 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.core.ws; + +import com.predic8.membrane.core.interceptor.schemavalidation.*; +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.wsdl.parser.*; +import com.predic8.membrane.core.util.wsdl.parser.schema.*; +import org.jetbrains.annotations.*; +import org.junit.jupiter.api.*; +import org.slf4j.*; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class WSDLIncludeImportTest { + + private static final Logger log = LoggerFactory.getLogger(WSDLIncludeImportTest.class); + + @Test + void test() throws Exception { + var defs = getDefinitions("classpath:/ws/include/include.wsdl"); + assertEquals(1, defs.getServices().size()); + assertEquals("http://example.com/test", defs.getTargetNamespace()); + assertEquals(1, defs.getSchemas().size()); + var embedded = defs.getSchemas().getFirst(); + assertEquals("http://example.com/test", embedded.getTargetNamespace()); + assertEquals(3, embedded.getSchemaElements().size()); + assertEquals(List.of("test", "testResponse", "du"), getElementNames(embedded.getSchemaElements())); + assertEquals(1, embedded.getImports().size()); + var imported = embedded.getImports().getFirst(); + assertEquals("http://example.com/test/data-types", imported.getNamespace()); + assertEquals(List.of("B5", "B6"), getElementNames(imported.getSchema().getSchemaElements())); + } + + @Test + void multiple() throws Exception { + assertEquals(List.of("test", "testResponse", "a", "b", "c"), + getElementNames(getDefinitions("classpath:/ws/include/multiple.wsdl") + .getSchemas().getFirst().getSchemaElements())); + } + + @Test + void cyclicInclude() throws Exception { + var defs = getDefinitions("classpath:/ws/include/cyclic.wsdl"); + assertEquals(1, defs.getSchemas().size()); + var embedded = defs.getSchemas().getFirst(); + assertEquals(1, embedded.getIncludes().size()); + assertEquals(List.of("test", "testResponse", "a", "b"), getElementNames(embedded.getSchemaElements())); + } + + @Test + void cyclicImport() throws Exception { + var defs = getDefinitions("classpath:/ws/import/cyclic.wsdl"); + assertEquals(1, defs.getSchemas().size()); + var embedded = defs.getSchemas().getFirst(); + var a = embedded.getImports().getFirst(); + assertEquals(List.of("a"), getElementNames(a.getSchema().getSchemaElements())); + var b = a.getSchema().getImports().getFirst(); + assertEquals(List.of("b"), getElementNames(b.getSchema().getSchemaElements())); + } + + + @Test + void messageReferencesImport() throws Exception { + var defs = getDefinitions("classpath:/ws/import/message-references-import.wsdl"); + assertEquals(1, defs.getSchemas().size()); + } + + @Test + void embeddedImports() throws Exception { + var defs = getDefinitions("classpath:/ws/import/embedded.wsdl"); + assertEquals(3, defs.getSchemas().size()); + var first = defs.getSchemas().getFirst(); + assertEquals(List.of("getCustomerRequest", "getCustomerResponse"), getElementNames(first.getSchemaElements())); + assertEquals(2, first.getImports().size()); + var firstImport = first.getImports().getFirst(); + assertEquals(List.of("from2"), getElementNames(firstImport.getSchema().getSchemaElements())); + var secondImport = first.getImports().get(1); + assertEquals(List.of("from3"), getElementNames(secondImport.getSchema().getSchemaElements())); + var second = defs.getSchemas().get(1); + assertEquals(List.of("from2"), getElementNames(second.getSchemaElements())); + var third = defs.getSchemas().get(2); + assertEquals(List.of("from3"), getElementNames(third.getSchemaElements())); + } + + @Test + void validator() { + var validator = new WSDLValidator(new ResolverMap(), + WSDLIncludeImportTest.class.getResource("/ws/include/include.wsdl").toString(), + "TestService", + (msg, exc) -> log.info("Validation failure: {}", msg), true); + validator.init(); + } + + private static @NotNull List getElementNames(List schemaElements) { + return schemaElements.stream().map(WSDLElement::getName).toList(); + } + + private static @NotNull Definitions getDefinitions(String location) throws Exception { + return Definitions.parse(new ResolverMap(), location); + } +} diff --git a/core/src/test/java/com/predic8/membrane/integration/withinternet/LargeBodyTest.java b/core/src/test/java/com/predic8/membrane/integration/withinternet/LargeBodyTest.java index fcb71f6856..9461779553 100644 --- a/core/src/test/java/com/predic8/membrane/integration/withinternet/LargeBodyTest.java +++ b/core/src/test/java/com/predic8/membrane/integration/withinternet/LargeBodyTest.java @@ -98,8 +98,8 @@ public static void shutdown() { public void large() throws Exception { long len = MAX_VALUE + 1L; - Exchange e = new Request.Builder().post("http://localhost:3041/foo").body(len, new ConstantInputStream(len)).buildExchange(); - try (HttpClient hc = new HttpClient(hcc)) { + var e = post("http://localhost:3041/foo").body(len, new ConstantInputStream(len)).buildExchange(); + try (var hc = new HttpClient(hcc)) { hc.call(e); } assertTrue(e.getRequest().getBody().wasStreamed()); diff --git a/core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java b/core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java index 36a5acdb62..a009fe6d1f 100644 --- a/core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java +++ b/core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java @@ -1,3 +1,17 @@ +/* Copyright 2026 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.integration.withoutinternet.interceptor; import com.predic8.membrane.core.interceptor.flow.ReturnInterceptor; diff --git a/core/src/test/resources/validation/inline-anytype.wsdl b/core/src/test/resources/validation/inline-anytype.wsdl index 3f3db659a1..ab66146966 100644 --- a/core/src/test/resources/validation/inline-anytype.wsdl +++ b/core/src/test/resources/validation/inline-anytype.wsdl @@ -1,76 +1,68 @@ - - + targetNamespace="http://www.examples.com/wsdl/HelloService.wsdl" + xmlns="http://schemas.xmlsoap.org/wsdl/" + xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" + xmlns:tns="http://www.examples.com/wsdl/HelloService.wsdl" + xmlns:ipo="http://www.example.com/IPO" +> - + + - + + + - - - + + + + + + + - - - - - + + + - + + + - + + + + + + - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - WSDL File for HelloService - - - - + + WSDL File for HelloService + + + + diff --git a/core/src/test/resources/ws/calculator-fault.wsdl b/core/src/test/resources/ws/calculator-fault.wsdl new file mode 100644 index 0000000000..000fbd6a16 --- /dev/null +++ b/core/src/test/resources/ws/calculator-fault.wsdl @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/hello-soap12.wsdl b/core/src/test/resources/ws/hello-soap12.wsdl new file mode 100644 index 0000000000..ed853491a0 --- /dev/null +++ b/core/src/test/resources/ws/hello-soap12.wsdl @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/cyclic.wsdl b/core/src/test/resources/ws/import/cyclic.wsdl new file mode 100644 index 0000000000..90f384b5c2 --- /dev/null +++ b/core/src/test/resources/ws/import/cyclic.wsdl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/cyclic/a.xsd b/core/src/test/resources/ws/import/cyclic/a.xsd new file mode 100644 index 0000000000..af11188e7a --- /dev/null +++ b/core/src/test/resources/ws/import/cyclic/a.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/cyclic/b.xsd b/core/src/test/resources/ws/import/cyclic/b.xsd new file mode 100644 index 0000000000..507401d43a --- /dev/null +++ b/core/src/test/resources/ws/import/cyclic/b.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/embedded.wsdl b/core/src/test/resources/ws/import/embedded.wsdl new file mode 100644 index 0000000000..4741fd3176 --- /dev/null +++ b/core/src/test/resources/ws/import/embedded.wsdl @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/message-references-import.wsdl b/core/src/test/resources/ws/import/message-references-import.wsdl new file mode 100644 index 0000000000..85f8b09853 --- /dev/null +++ b/core/src/test/resources/ws/import/message-references-import.wsdl @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/import/messages.xsd b/core/src/test/resources/ws/import/messages.xsd new file mode 100644 index 0000000000..467bfc4308 --- /dev/null +++ b/core/src/test/resources/ws/import/messages.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/cyclic.wsdl b/core/src/test/resources/ws/include/cyclic.wsdl new file mode 100644 index 0000000000..416a455a20 --- /dev/null +++ b/core/src/test/resources/ws/include/cyclic.wsdl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/cyclic/a.xsd b/core/src/test/resources/ws/include/cyclic/a.xsd new file mode 100644 index 0000000000..facef884c8 --- /dev/null +++ b/core/src/test/resources/ws/include/cyclic/a.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/cyclic/b.xsd b/core/src/test/resources/ws/include/cyclic/b.xsd new file mode 100644 index 0000000000..17bcc0fb7b --- /dev/null +++ b/core/src/test/resources/ws/include/cyclic/b.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/include.wsdl b/core/src/test/resources/ws/include/include.wsdl new file mode 100644 index 0000000000..9375ea8bcf --- /dev/null +++ b/core/src/test/resources/ws/include/include.wsdl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/include.xsd b/core/src/test/resources/ws/include/include.xsd new file mode 100644 index 0000000000..47309d5a33 --- /dev/null +++ b/core/src/test/resources/ws/include/include.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/multiple.wsdl b/core/src/test/resources/ws/include/multiple.wsdl new file mode 100644 index 0000000000..414e5347b5 --- /dev/null +++ b/core/src/test/resources/ws/include/multiple.wsdl @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/multiple/a.xsd b/core/src/test/resources/ws/include/multiple/a.xsd new file mode 100644 index 0000000000..facef884c8 --- /dev/null +++ b/core/src/test/resources/ws/include/multiple/a.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/multiple/b.xsd b/core/src/test/resources/ws/include/multiple/b.xsd new file mode 100644 index 0000000000..601c940af2 --- /dev/null +++ b/core/src/test/resources/ws/include/multiple/b.xsd @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/multiple/c.xsd b/core/src/test/resources/ws/include/multiple/c.xsd new file mode 100644 index 0000000000..bb66053a6c --- /dev/null +++ b/core/src/test/resources/ws/include/multiple/c.xsd @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/xsd/inc/MessageStructureTypes2.xsd b/core/src/test/resources/ws/include/xsd/inc/MessageStructureTypes2.xsd new file mode 100644 index 0000000000..9bb959d784 --- /dev/null +++ b/core/src/test/resources/ws/include/xsd/inc/MessageStructureTypes2.xsd @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/xsd/inc/shared/xsd/DataTypes.xsd b/core/src/test/resources/ws/include/xsd/inc/shared/xsd/DataTypes.xsd new file mode 100644 index 0000000000..3e9bd480ca --- /dev/null +++ b/core/src/test/resources/ws/include/xsd/inc/shared/xsd/DataTypes.xsd @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/xsd/inc/shared/xsd/inc/BaseTypes.xsd b/core/src/test/resources/ws/include/xsd/inc/shared/xsd/inc/BaseTypes.xsd new file mode 100644 index 0000000000..e8b26c8fbe --- /dev/null +++ b/core/src/test/resources/ws/include/xsd/inc/shared/xsd/inc/BaseTypes.xsd @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/core/src/test/resources/ws/include/xsd/messages2.xsd b/core/src/test/resources/ws/include/xsd/messages2.xsd new file mode 100644 index 0000000000..ad2f982f60 --- /dev/null +++ b/core/src/test/resources/ws/include/xsd/messages2.xsd @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/distribution/examples/extending-membrane/error-handling/custom-error-messages/apis.yaml b/distribution/examples/extending-membrane/error-handling/custom-error-messages/apis.yaml index aaf1d5b213..7cadc07397 100644 --- a/distribution/examples/extending-membrane/error-handling/custom-error-messages/apis.yaml +++ b/distribution/examples/extending-membrane/error-handling/custom-error-messages/apis.yaml @@ -1,4 +1,12 @@ # yaml-language-server: $schema=https://www.membrane-api.io/v7.1.2.json +components: + ns: + xmlConfig: + namespaces: + - prefix: s11 + uri: "http://schemas.xmlsoap.org/soap/envelope/" + +--- api: port: 2000 path: @@ -24,28 +32,25 @@ api: test: isXML() flow: - if: + test: //s11:Fault language: xpath - test: //*[local-name() = 'Fault' and namespace-uri() = 'http://schemas.xmlsoap.org/soap/envelope/'] + $ref: "#/components/ns" flow: - template: contentType: application/xml src: | - + e ${statusCode} SOAP Fault! - ${property.faultstring} + ${xpath('//faultstring/text()')} - - setProperty: - name: faultstring - value: ${//faultstring/text()} - language: xpath - if: test: statusCode >= 400 flow: - if: + test: not(/s11:Envelope) language: xpath - test: /*[not(local-name() = 'Envelope')] flow: - template: contentType: application/xml @@ -53,12 +58,8 @@ api: d ${statusCode} - ${property.description} + ${xpath('/failure/description')} - - setProperty: - name: description - value: ${/failure/description} - language: xpath - abort: - if: test: header['X-Protection'] != null diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/custom_error_messages/CustomErrorHandlingExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/custom_error_messages/CustomErrorHandlingExampleTest.java index d49dc5ff75..d4d69b88a4 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/custom_error_messages/CustomErrorHandlingExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/custom_error_messages/CustomErrorHandlingExampleTest.java @@ -84,6 +84,7 @@ void caseD() { .when() .get("http://localhost:2000/service") .then() + .log().ifValidationFails() .statusCode(500) .contentType(XML) .body("error.case", equalTo("d")) @@ -98,18 +99,17 @@ void caseE() { - Verursache SOAP Fault! + Düsseldorf """.stripIndent()) .when() .post("http://localhost:2000/service") .then() + .log().ifValidationFails() .statusCode(200) - .body( - containsString("e"), - containsString("Not Found") - ); + .body("error.case", equalTo("e")) + .body("error.fault", equalTo("Resource Not Found")); } @Test diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 394385cc2b..e21d6a157b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,6 +4,8 @@ - Correct YAML example on GitHub README + + # 7.2.0 - Move SOAP samples from tutorials/transformations to ../soap TB @@ -52,7 +54,9 @@ PRIO 2: - PRIO 3: - +- WebServiceExplorerInterceptor + - Support multiple services, ports, ... + - Refactor - JMXExporter: - Tutorial - Example diff --git a/pom.xml b/pom.xml index 2b840f8d31..e46b76adb9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,8 @@ 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. ---> +--> + 4.0.0 org.membrane-soa @@ -83,7 +84,6 @@ 4.0.29 - @@ -156,6 +156,11 @@ groovy-templates ${groovy.version} + + org.apache.groovy + groovy-json + ${groovy.version} + com.fasterxml.jackson jackson-bom @@ -224,11 +229,6 @@ 6.1.0 provided - - com.predic8 - soa-model-core - 2.2.0 - com.google.guava guava