Item 258: JAXB types of a WSDL's schemas are linked to it; soap-endpoints lists typesPackages and their users as clients

This commit is contained in:
Ingo Schnabel
2026-09-26 21:47:12 +02:00
parent 0264bd554e
commit a0e23c6594
6 changed files with 146 additions and 7 deletions

View File

@@ -81,7 +81,9 @@ class KorinGapsCIT {
write(clientRoot, "src/main/resources/wsdl/korin/WS_Leitwaehrung.wsdl", """
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:x">
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:x"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:types><xsd:schema targetNamespace="urn:com-softwareag-entirex-rpc:WSK386"/></wsdl:types>
<wsdl:portType name="WSK386Port"><wsdl:operation name="getData"/></wsdl:portType>
<wsdl:binding name="B" type="tns:WSK386Port"><soap:binding style="document"/></wsdl:binding>
<wsdl:service name="WS_Leitwaehrung">
@@ -111,6 +113,18 @@ class KorinGapsCIT {
}
}
""");
// item 258: a class that only uses the request type JAXB generates from the WSDL's schema
write(clientRoot, "src/main/java/at/k3/base/ws/wsk386/WSK386Request.java", """
package at.k3.base.ws.wsk386;
import rpc.entirex.softwareag.com.wsk386.GetData;
public class WSK386Request {
public GetData build() {
return new GetData();
}
}
""");
write(clientRoot, "src/main/java/at/k3/ws/Stete.java", """
package at.k3.ws;
import jakarta.jws.WebMethod;
@@ -221,7 +235,10 @@ class KorinGapsCIT {
void wsimportBindingLinksTheGeneratedClientToItsWsdl() {
given().when().get(api(CLIENT, "/soap-endpoints?kind=wsdl&service=WS_Leitwaehrung")).then().statusCode(200)
.body("[0].clientPackage", equalTo("at.k3.entirex.ws.wsk386"))
.body("[0].clients", contains("at.k3.base.ws.wsk386.WSK386GetDataService"))
// item 258: the user of the schema's JAXB types is a client too
.body("[0].clients", containsInAnyOrder("at.k3.base.ws.wsk386.WSK386GetDataService",
"at.k3.base.ws.wsk386.WSK386Request"))
.body("[0].typesPackages", equalTo("rpc.entirex.softwareag.com.wsk386"))
.body("[0].counterpartProject", equalTo(SERVICE));
given().when().get(api(CLIENT, "/modules?moduleKind=WSIMPORT_BINDING")).then().statusCode(200)
.body("name", contains("src/main/resources/wsdl/korin/WS_Leitwaehrung-binding.xml"));

View File

@@ -4906,6 +4906,32 @@ public final class CypherQueries {
MERGE (ph)-[r:COUNTERPART_OF]->(w)
SET r.via = 'wsimport'
""";
/**
* Item 258: the request/response classes JAXB generates from a WSDL's schemas
* ({@code rpc.entirex.softwareag.com.wsk369.GetData}) are, like the item-239 client, not in the sources —
* placeholders. Each is linked {@code COUNTERPART_OF {via: jaxb}} to the WSDL service whose
* {@code typesPackages} names its package, when exactly one service does. Top-level classes of the
* package only (the package is the name up to the last dot). The placeholders are read once and the
* packages tested with {@code IN}, which is hashed.
*/
public static final String LINK_WSDL_TYPES = """
MATCH (w:MODULE {project: $project})
WHERE w.moduleKind = 'WSDL_SERVICE' AND w.typesPackages IS NOT NULL
UNWIND split(w.typesPackages, ',') AS pkg
WITH pkg, collect(w) AS ws
WHERE size(ws) = 1
WITH collect({pkg: pkg, w: ws[0]}) AS owned
WITH owned, [x IN owned | x.pkg] AS pkgs
WHERE size(owned) > 0
MATCH (ph:AstNode {project: $project, sourceFile: "", type: 'MODULE'})
WHERE ph.name CONTAINS '.'
WITH owned, pkgs, ph, substring(ph.name, 0, size(ph.name) - size(last(split(ph.name, '.'))) - 1) AS pkg
WHERE pkg IN pkgs
WITH ph, head([x IN owned WHERE x.pkg = pkg | x.w]) AS w
MERGE (ph)-[r:COUNTERPART_OF]->(w)
SET r.via = 'jaxb'
""";
/**
* Item 225: a WSDL a client project keeps for a service (K3's {@code wsdl/korinX/WS_X.wsdl}) is linked
* to the counterpart project's Axis2 {@code SOAP_SERVICE} of the same name — unique match only. With
@@ -5218,14 +5244,16 @@ public final class CypherQueries {
// clientPackage) — computed for the page only.
CALL {
WITH node
OPTIONAL MATCH (node)<-[:COUNTERPART_OF {via: 'wsimport'}]-(:AstNode)<-[:MENTIONS|CALLS|REFERENCES|INJECTS]-(src:AstNode)
// item 258: users of the schema types (via jaxb) are clients as well
OPTIONAL MATCH (node)<-[cp:COUNTERPART_OF]-(:AstNode)<-[:MENTIONS|CALLS|REFERENCES|INJECTS]-(src:AstNode)
WHERE cp.via IN ['wsimport', 'jaxb']
OPTIONAL MATCH (cm:MODULE {project: $project})-[:CONTAINS*0..1]->(src)
WHERE cm.sourceFile <> ''
RETURN [x IN collect(DISTINCT cm.name) WHERE x IS NOT NULL] AS clients
}
RETURN kind, service, operation, implementation, sourceFile, line, location, port,
c.project AS counterpartProject, c.name AS counterpartService,
node.clientPackage AS clientPackage, clients
node.clientPackage AS clientPackage, clients, node.typesPackages AS typesPackages
""";
public static final String SOAP_ENDPOINTS_COUNT = SOAP_ENDPOINTS_CORE + """
RETURN count(*) AS total

View File

@@ -345,7 +345,8 @@ public class GraphRepository {
new EnrichmentStep("link-counterparts-field", CypherQueries.LINK_COUNTERPARTS_FIELD),
new EnrichmentStep("link-counterparts-natjav", CypherQueries.LINK_COUNTERPARTS_NATJAV),
new EnrichmentStep("link-counterparts-soap", CypherQueries.LINK_COUNTERPARTS_SOAP),
new EnrichmentStep("link-wsimport-clients", CypherQueries.LINK_WSIMPORT_CLIENTS));
new EnrichmentStep("link-wsimport-clients", CypherQueries.LINK_WSIMPORT_CLIENTS),
new EnrichmentStep("link-wsdl-types", CypherQueries.LINK_WSDL_TYPES));
/**
* Strips a single leading Natural sigil ({@code #}, {@code &}, {@code +}) so name search is sigil-insensitive.
@@ -3105,7 +3106,8 @@ public class GraphRepository {
record.get("sourceFile").asString(""), intOrNull(record.get("line")),
nullableString(record, "location"), nullableString(record, "counterpartProject"),
nullableString(record, "counterpartService"), nullableString(record, "port"),
nullableString(record, "clientPackage"), record.get("clients").asList(Value::asString)))
nullableString(record, "clientPackage"), record.get("clients").asList(Value::asString),
nullableString(record, "typesPackages")))
.flatMap(rows -> withTotal(rows, limit, offset, () -> count(CypherQueries.SOAP_ENDPOINTS_COUNT, params)));
}

View File

@@ -23,9 +23,12 @@ import java.util.List;
* @param clientPackage item 239: for a WSDL row, the Java package its wsimport client is generated into
* ({@code at.k3.entirex.ws.wsk386}), from the project's wsimport binding file
* @param clients item 239: the modules importing or calling a class of that package
* @param typesPackages item 258: for a WSDL row, the comma-separated Java packages JAXB generates the
* request/response classes of its schemas into ({@code rpc.entirex.softwareag.com.wsk369})
*/
public record SoapEndpoint(String kind, String service, @Nullable String operation, @Nullable String implementation,
String sourceFile, @Nullable Integer line, @Nullable String location,
@Nullable String counterpartProject, @Nullable String counterpartService,
@Nullable String port, @Nullable String clientPackage, List<String> clients) {
@Nullable String port, @Nullable String clientPackage, List<String> clients,
@Nullable String typesPackages) {
}

View File

@@ -198,6 +198,7 @@ public final class SoapDescriptorParser {
Map<String, Map<String, Integer>> portTypeOperations = new LinkedHashMap<>();
Map<String, String> bindingPortType = new LinkedHashMap<>();
List<Service> services = new ArrayList<>();
java.util.Set<String> typesPackages = new java.util.LinkedHashSet<>();
read(content, new DefaultHandler() {
private @Nullable Locator locator;
private @Nullable String portType;
@@ -250,6 +251,14 @@ public final class SoapDescriptorParser {
service.location = a.get("location");
}
}
// Item 258: the JAXB classes of an embedded schema land in the package its namespace
// maps to (urn:com-softwareag-entirex-rpc:WSK369 -> rpc.entirex.softwareag.com.wsk369)
case "schema" -> {
String pkg = a.containsKey("targetNamespace") ? jaxbPackage(a.get("targetNamespace")) : null;
if (pkg != null) {
typesPackages.add(pkg);
}
}
default -> {
}
}
@@ -286,6 +295,9 @@ public final class SoapDescriptorParser {
if (s.port != null) {
props.put("port", s.port);
}
if (!typesPackages.isEmpty()) {
props.put("typesPackages", String.join(",", typesPackages));
}
AstNode module = new AstNode(UUID.randomUUID(), NodeType.MODULE, sourceFile + "#" + s.name, sourceFile,
ConfigFileParser.LANGUAGE, s.line, lineCount, null, null, props);
nodes.add(module);
@@ -310,6 +322,47 @@ public final class SoapDescriptorParser {
*/
private static final Pattern ACTION_PORT = Pattern.compile("/([A-Za-z0-9_]+)Port/");
/**
* Item 258: the Java package JAXB derives from a namespace URI (JAXB 2.x, appendix D.5.1): scheme
* dropped, split at {@code /} and {@code :}, a URN's first part with dashes read as dots, the host
* part reversed without a leading {@code www}, everything lower-cased, characters no identifier may
* hold replaced by {@code _}. {@code http://namespace.softwareag.com/entirex/xml/mapping} →
* {@code com.softwareag.namespace.entirex.xml.mapping}. A binding customisation that names another
* package is not read. {@code null} for an empty namespace.
*/
static @Nullable String jaxbPackage(String namespace) {
String ns = namespace.trim();
boolean urn = ns.regionMatches(true, 0, "urn:", 0, 4);
ns = ns.replaceFirst("^[A-Za-z][A-Za-z0-9+.-]*:(//)?", "");
ns = ns.replaceFirst("\\.(xsd|wsdl|xml|html?)$", "");
List<String> tokens = new ArrayList<>();
for (String token : ns.split("[/:]")) {
if (!token.isEmpty()) {
tokens.add(token);
}
}
if (tokens.isEmpty()) {
return null;
}
String host = urn ? tokens.getFirst().replace('-', '.') : tokens.getFirst();
List<String> hostParts = new ArrayList<>(java.util.Arrays.asList(host.split("\\.")));
if (!hostParts.isEmpty() && hostParts.getFirst().equalsIgnoreCase("www")) {
hostParts.removeFirst();
}
java.util.Collections.reverse(hostParts);
List<String> parts = new ArrayList<>(hostParts);
parts.addAll(tokens.subList(1, tokens.size()));
List<String> segments = new ArrayList<>();
for (String part : parts) {
String segment = part.toLowerCase(java.util.Locale.ROOT).replaceAll("[^a-z0-9_]", "_");
if (segment.isEmpty()) {
continue;
}
segments.add(Character.isDigit(segment.charAt(0)) ? "_" + segment : segment);
}
return segments.isEmpty() ? null : String.join(".", segments);
}
/**
* Item 238: {@code WSK369Port} → {@code WSK369}; a portType without the suffix is the port itself.
*/

View File

@@ -152,4 +152,40 @@ class ConfigAndSoapParserTest {
assertEquals(1, result.nodes().stream().filter(n -> n.type() == NodeType.FUNCTION).count());
assertEquals("getData", node(result, NodeType.FUNCTION, "getData").name());
}
/**
* Item 258: the JAXB package of a namespace — checked against a name the corpus confirms: the korin-ws
* skeleton {@code com.softwareag.namespace.entirex.xml.mapping.WS_KontoSkeleton} is generated for the
* namespace {@code http://namespace.softwareag.com/entirex/xml/mapping}.
*/
@Test
void jaxbDerivesThePackageFromTheNamespace() {
assertEquals("com.softwareag.namespace.entirex.xml.mapping",
SoapDescriptorParser.jaxbPackage("http://namespace.softwareag.com/entirex/xml/mapping"));
assertEquals("rpc.entirex.softwareag.com.wsk369",
SoapDescriptorParser.jaxbPackage("urn:com-softwareag-entirex-rpc:WSK369"));
assertEquals("com.example.types.v1",
SoapDescriptorParser.jaxbPackage("https://www.example.com/types/v1.xsd"));
assertNull(SoapDescriptorParser.jaxbPackage(""));
}
@Test
void aWsdlNamesThePackagesOfItsSchemaTypes() {
ParseResult result = SoapDescriptorParser.parse("src/main/resources/wsdl/korinKonto/WS_Konto.wsdl", """
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="WS_Konto" targetNamespace="http://namespace.softwareag.com/entirex/xml/mapping"
xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://namespace.softwareag.com/entirex/xml/mapping">
<types>
<xsd:schema targetNamespace="urn:com-softwareag-entirex-rpc:WSK369"/>
</types>
<portType name="WSK369Port"><operation name="getData"/></portType>
<binding name="WSK369Binding" type="tns:WSK369Port"/>
<service name="WS_Konto"><port name="WSK369Port" binding="tns:WSK369Binding"/></service>
</definitions>
""");
AstNode service = node(result, NodeType.MODULE, "src/main/resources/wsdl/korinKonto/WS_Konto.wsdl#WS_Konto");
assertEquals("rpc.entirex.softwareag.com.wsk369", prop(service, "typesPackages"));
assertEquals("WSK369", prop(service, "port"));
}
}