Skip to content

Commit 4ac12e7

Browse files
committed
feat(soap): SOAP response body uses typed message part names.
1 parent 37a9836 commit 4ac12e7

File tree

7 files changed

+129
-56
lines changed

7 files changed

+129
-56
lines changed

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/model/OperationMessage.kt

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,41 +50,33 @@ import javax.xml.namespace.QName
5050
* `part` element within a `message`. In WSDL 2.0 this is an `input` or `output`
5151
* element within an `operation`.
5252
*/
53-
abstract class OperationMessage(
54-
val operationName: String,
55-
)
53+
abstract class OperationMessage
5654

5755
/**
5856
* Refers to an XML schema `element`.
5957
*/
6058
class ElementOperationMessage(
61-
operationName: String,
6259
val elementName: QName,
63-
) : OperationMessage(
64-
operationName
65-
)
60+
) : OperationMessage()
6661

6762
/**
6863
* Message parts specifying an XML schema `type` are supported
6964
* in WSDL 1.1 but not in WSDL 2.0.
7065
*/
7166
class TypeOperationMessage(
72-
operationName: String,
67+
val operationName: String,
68+
val partName: String,
7369
val typeName: QName,
74-
): OperationMessage(
75-
operationName
76-
)
70+
): OperationMessage()
7771

7872
/**
7973
* In WSDL 1.1, messages can define multiple parts.
8074
*/
8175
class CompositeOperationMessage(
82-
operationName: String,
76+
val operationName: String,
8377

8478
/**
8579
* Maps the `part` name to an operation message.
8680
*/
87-
val parts: Map<String, OperationMessage>
88-
): OperationMessage(
89-
operationName
90-
)
81+
val parts: List<OperationMessage>
82+
): OperationMessage()

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/parser/AbstractWsdlParser.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,14 @@ abstract class AbstractWsdlParser(
172172
* from within the XSD.
173173
*/
174174
protected fun resolveTypeFromXsd(typeQName: QName): QName? {
175-
val matchingType = unionTypeSystem.findType(typeQName)?.name
175+
var matchingType = unionTypeSystem.findType(typeQName)?.name
176176
?: return null
177177

178+
if (matchingType.prefix.isNullOrBlank()) {
179+
// TODO consider prefix clashes - generate unique prefix?
180+
matchingType = QName(matchingType.namespaceURI, matchingType.localPart, typeQName.prefix)
181+
}
182+
178183
logger.trace("Resolved type name {} to qualified type: {}", typeQName, matchingType)
179184
return matchingType
180185
}

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/parser/Wsdl1Parser.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,25 +230,23 @@ class Wsdl1Parser(
230230
// look up message parts
231231
val messageParts = selectNodes(message, "./wsdl:part")
232232

233-
val parts: Map<String, OperationMessage> = messageParts.associate { messagePart ->
233+
val parts: List<OperationMessage> = messageParts.map { messagePart ->
234234
val partName = messagePart.getAttributeValue("name")
235235

236236
// WSDL 1.1 allows message parts to refer to XML schema types
237237
// directly as well as referring to elements.
238-
val part = getAttributeValueAsQName(messagePart, "element")?.let { elementQName ->
239-
resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(operationName, it) }
238+
getAttributeValueAsQName(messagePart, "element")?.let { elementQName ->
239+
resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(it) }
240240
} ?: getAttributeValueAsQName(messagePart, "type")?.let { typeQName ->
241-
resolveTypeFromXsd(typeQName)?.let { TypeOperationMessage(operationName, it) }
241+
resolveTypeFromXsd(typeQName)?.let { TypeOperationMessage(operationName, partName, it) }
242242
} ?: throw IllegalStateException(
243243
"Invalid 'element' or 'type' attribute for message: $messageName"
244244
)
245-
246-
partName to part
247245
}
248246

249247
return when (parts.size) {
250248
0 -> return null
251-
1 -> parts.values.first()
249+
1 -> parts.first()
252250
else -> CompositeOperationMessage(operationName, parts)
253251
}
254252
}

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/parser/Wsdl2Parser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class Wsdl2Parser(
185185
// WSDL 2.0 doesn't allow operation messages to refer to XML schema types
186186
// directly - instead an element must be used.
187187
getAttributeValueAsQName(inputOrOutputNode, "element")?.let { elementQName ->
188-
return resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(operationName, it) }
188+
return resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(it) }
189189
} ?: throw IllegalStateException(
190190
"Invalid 'element' attribute for message input/output: ${inputOrOutputNode.name}"
191191
)

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/service/SoapExampleService.kt

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import io.gatehill.imposter.plugin.soap.model.ParsedRawBody
5151
import io.gatehill.imposter.plugin.soap.model.ParsedSoapMessage
5252
import io.gatehill.imposter.plugin.soap.model.TypeOperationMessage
5353
import io.gatehill.imposter.plugin.soap.parser.WsdlRelativeXsdEntityResolver
54+
import io.gatehill.imposter.plugin.soap.util.SchemaGenerator
5455
import io.gatehill.imposter.plugin.soap.util.SoapUtil
5556
import io.gatehill.imposter.util.LogUtil
5657
import org.apache.logging.log4j.LogManager
@@ -79,64 +80,54 @@ class SoapExampleService {
7980
httpExchange: HttpExchange,
8081
schemas: Array<SchemaDocument>,
8182
wsdlDir: File,
82-
outputRef: OperationMessage,
83+
outputMessage: OperationMessage,
8384
bodyHolder: MessageBodyHolder,
8485
): Boolean {
85-
logger.debug("Generating example for {}", outputRef)
86-
val example = generateInstanceFromSchemas(schemas, wsdlDir, outputRef)
86+
logger.debug("Generating example for {}", outputMessage)
87+
val example = generateInstanceFromSchemas(schemas, wsdlDir, outputMessage)
8788
transmitExample(httpExchange, example, bodyHolder)
8889
return true
8990
}
9091

9192
private fun generateInstanceFromSchemas(
9293
schemas: Array<SchemaDocument>,
9394
wsdlDir: File,
94-
typeRef: OperationMessage,
95+
message: OperationMessage,
9596
): String {
96-
when (typeRef) {
97+
when (message) {
9798
is ElementOperationMessage -> {
9899
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)
99100

100101
// TODO should this use the qualified name instead?
101-
val rootElementName = typeRef.elementName.localPart
102+
val rootElementName = message.elementName.localPart
102103
val elem: SchemaType = sts.documentTypes().find { it.documentElementName.localPart == rootElementName }
103104
?: throw RuntimeException("Could not find a global element with name \"$rootElementName\"")
104105

105106
return SampleXmlUtil.createSampleForType(elem)
106107
}
108+
107109
is TypeOperationMessage -> {
108110
// by convention, the suffix 'Response' is added to the operation name
109-
val rootElementName = QName(
110-
typeRef.typeName.namespaceURI,
111-
typeRef.operationName + "Response",
111+
val rootElement = QName(
112+
message.typeName.namespaceURI,
113+
message.operationName + "Response",
112114
"tns"
113115
)
114-
val elementSchema = createElementSchema(rootElementName, typeRef.typeName)
116+
117+
val parts = mutableMapOf<String, QName>()
118+
parts[message.partName] = message.typeName
119+
120+
val elementSchema = SchemaGenerator.createElementSchema(rootElement, parts)
115121
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)
116122

117-
val elem = sts.documentTypes().find { it.documentElementName == rootElementName }
118-
?: throw RuntimeException("Could not find a generated element with name \"$rootElementName\"")
123+
val elem = sts.documentTypes().find { it.documentElementName == rootElement }
124+
?: throw RuntimeException("Could not find a generated element with name \"$rootElement\"")
119125

120126
return SampleXmlUtil.createSampleForType(elem)
121127
}
122-
else -> throw UnsupportedOperationException("Only element output message parts are supported")
123-
}
124-
}
125128

126-
private fun createElementSchema(elementName: QName, typeName: QName): SchemaDocument {
127-
val schemaXml = """
128-
<xs:schema elementFormDefault="unqualified" version="1.0"
129-
xmlns:xs="http://www.w3.org/2001/XMLSchema"
130-
targetNamespace="${elementName.namespaceURI}"
131-
xmlns:${elementName.prefix}="${elementName.namespaceURI}">
132-
133-
<!-- use the element prefix as the type QName may not have one -->
134-
<xs:element name="${elementName.localPart}" type="${elementName.prefix}:${typeName.localPart}"/>
135-
</xs:schema>
136-
""".trim()
137-
138-
logger.trace("Generated element schema:\n{}", schemaXml)
139-
return SchemaDocument.Factory.parse(schemaXml)
129+
else -> throw UnsupportedOperationException("Unsupported output message parts: ${message::class.java.canonicalName}")
130+
}
140131
}
141132

142133
private fun buildSchemaTypeSystem(
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright (c) 2024.
3+
*
4+
* This file is part of Imposter.
5+
*
6+
* "Commons Clause" License Condition v1.0
7+
*
8+
* The Software is provided to you by the Licensor under the License, as
9+
* defined below, subject to the following condition.
10+
*
11+
* Without limiting other conditions in the License, the grant of rights
12+
* under the License will not include, and the License does not grant to
13+
* you, the right to Sell the Software.
14+
*
15+
* For purposes of the foregoing, "Sell" means practicing any or all of
16+
* the rights granted to you under the License to provide to third parties,
17+
* for a fee or other consideration (including without limitation fees for
18+
* hosting or consulting/support services related to the Software), a
19+
* product or service whose value derives, entirely or substantially, from
20+
* the functionality of the Software. Any license notice or attribution
21+
* required by the License must also include this Commons Clause License
22+
* Condition notice.
23+
*
24+
* Software: Imposter
25+
*
26+
* License: GNU Lesser General Public License version 3
27+
*
28+
* Licensor: Peter Cornish
29+
*
30+
* Imposter is free software: you can redistribute it and/or modify
31+
* it under the terms of the GNU Lesser General Public License as published by
32+
* the Free Software Foundation, either version 3 of the License, or
33+
* (at your option) any later version.
34+
*
35+
* Imposter is distributed in the hope that it will be useful,
36+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
37+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38+
* GNU Lesser General Public License for more details.
39+
*
40+
* You should have received a copy of the GNU Lesser General Public License
41+
* along with Imposter. If not, see <https://www.gnu.org/licenses/>.
42+
*/
43+
44+
package io.gatehill.imposter.plugin.soap.util
45+
46+
import org.apache.logging.log4j.LogManager
47+
import org.apache.logging.log4j.Logger
48+
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument
49+
import javax.xml.namespace.QName
50+
51+
/**
52+
* Generates schemas for elements.
53+
*/
54+
object SchemaGenerator {
55+
private val logger: Logger = LogManager.getLogger(SchemaGenerator::class.java)
56+
57+
fun createElementSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
58+
val namespaces = mutableMapOf<String, String>()
59+
namespaces[rootElement.prefix] = rootElement.namespaceURI
60+
namespaces += parts.values.associate { it.prefix to it.namespaceURI }
61+
62+
val namespacesXml = namespaces.entries.joinToString(separator = "\n") { (prefix, nsUri) ->
63+
"""xmlns:${prefix}="${nsUri}""""
64+
}
65+
val partsXml = parts.entries.joinToString(separator = "\n") { (partName, partType) ->
66+
"""<xs:element name="$partName" type="${partType.prefix}:${partType.localPart}"/>"""
67+
}
68+
val schemaXml = """
69+
<xs:schema elementFormDefault="unqualified" version="1.0"
70+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
71+
${namespacesXml.prependIndent(" ".repeat(11))}
72+
targetNamespace="${rootElement.namespaceURI}">
73+
74+
<xs:element name="${rootElement.localPart}">
75+
<xs:complexType>
76+
<xs:sequence>
77+
${partsXml.prependIndent(" ".repeat(10))}
78+
</xs:sequence>
79+
</xs:complexType>
80+
</xs:element>
81+
</xs:schema>
82+
""".trim()
83+
84+
logger.trace("Generated element schema:\n{}", schemaXml)
85+
return SchemaDocument.Factory.parse(schemaXml)
86+
}
87+
}

mock/soap/src/test/resources/wsdl1-soap11-message-type/service.wsdl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@
8787
<part type="tns:getPetByIdRequest" name="parameters"/>
8888
</message>
8989
<message name="getPetByIdResponse">
90-
<part type="tns:petType" name="parameters"/>
90+
<part type="tns:petType" name="pet"/>
9191
</message>
9292
<message name="getPetByNameRequest">
9393
<part type="tns:getPetByNameRequest" name="parameters"/>
9494
</message>
9595
<message name="getPetByNameResponse">
96-
<part type="tns:petType" name="parameters"/>
96+
<part type="tns:petType" name="pet"/>
9797
</message>
9898

9999
<!-- Abstract port types -->

0 commit comments

Comments
 (0)