Skip to content

Commit d8556af

Browse files
committed
feat(soap): improves support for element parts in composite messages.
1 parent 211a1f7 commit d8556af

File tree

10 files changed

+315
-24
lines changed

10 files changed

+315
-24
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ 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
53+
interface OperationMessage
5454

5555
/**
5656
* Refers to an XML schema `element`.
5757
*/
5858
class ElementOperationMessage(
5959
val elementName: QName,
60-
) : OperationMessage()
60+
val elementType: QName,
61+
) : OperationMessage
6162

6263
/**
6364
* Message parts specifying an XML schema `type` are supported
@@ -67,7 +68,7 @@ class TypeOperationMessage(
6768
val operationName: String,
6869
val partName: String,
6970
val typeName: QName,
70-
): OperationMessage()
71+
): OperationMessage
7172

7273
/**
7374
* In WSDL 1.1, messages can define multiple parts.
@@ -79,4 +80,4 @@ class CompositeOperationMessage(
7980
* Maps the `part` name to an operation message.
8081
*/
8182
val parts: List<OperationMessage>
82-
): OperationMessage()
83+
): OperationMessage

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,17 @@ abstract class AbstractWsdlParser(
159159
val matchingElement = xsd.findElement(elementQName)
160160
?: return null
161161

162-
val elementType = matchingElement.type.name
162+
var elementType = matchingElement.type.name
163+
164+
if (elementType.prefix.isNullOrBlank()) {
165+
val prefix = if (elementType.namespaceURI == "http://www.w3.org/2001/XMLSchema") {
166+
"xs"
167+
} else {
168+
// TODO consider prefix clashes - generate unique prefix?
169+
elementQName.prefix
170+
}
171+
elementType = QName(elementType.namespaceURI, elementType.localPart, prefix)
172+
}
163173

164174
logger.trace("Resolved element name {} to qualified type: {}", elementQName, elementType)
165175
return elementType
@@ -174,8 +184,13 @@ abstract class AbstractWsdlParser(
174184
?: return null
175185

176186
if (matchingType.prefix.isNullOrBlank()) {
177-
// TODO consider prefix clashes - generate unique prefix?
178-
matchingType = QName(matchingType.namespaceURI, matchingType.localPart, typeQName.prefix)
187+
val prefix = if (matchingType.namespaceURI == "http://www.w3.org/2001/XMLSchema") {
188+
"xs"
189+
} else {
190+
// TODO consider prefix clashes - generate unique prefix?
191+
typeQName.prefix
192+
}
193+
matchingType = QName(matchingType.namespaceURI, matchingType.localPart, prefix)
179194
}
180195

181196
logger.trace("Resolved type name {} to qualified type: {}", typeQName, matchingType)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class Wsdl1Parser(
240240
// WSDL 1.1 allows message parts to refer to XML schema types
241241
// directly as well as referring to elements.
242242
getAttributeValueAsQName(messagePart, "element")?.let { elementQName ->
243-
resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(it) }
243+
resolveElementTypeFromXsd(elementQName)?.let { ElementOperationMessage(elementQName, it) }
244244
} ?: getAttributeValueAsQName(messagePart, "type")?.let { typeQName ->
245245
resolveTypeFromXsd(typeQName)?.let { TypeOperationMessage(operationName, partName, it) }
246246
} ?: throw IllegalStateException(

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
@@ -189,7 +189,7 @@ class Wsdl2Parser(
189189
// WSDL 2.0 doesn't allow operation messages to refer to XML schema types
190190
// directly - instead an element must be used.
191191
getAttributeValueAsQName(inputOrOutputNode, "element")?.let { elementQName ->
192-
return resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(it) }
192+
return resolveElementTypeFromXsd(elementQName)?.let { ElementOperationMessage(elementQName, it) }
193193
} ?: throw IllegalStateException(
194194
"Invalid 'element' attribute for message input/output: ${inputOrOutputNode.name}"
195195
)

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,28 +147,30 @@ class SoapExampleService {
147147

148148
when (val message = operation.outputRef) {
149149
is ElementOperationMessage -> {
150-
// TODO generate wrapper root element and place element part XML within it
151-
TODO("Element parts in RPC bindings are not supported")
150+
// FIXME replicate composite element logic branch below
151+
parts[message.elementName.localPart] = message.elementType
152152
}
153153

154154
is TypeOperationMessage -> {
155155
parts[message.partName] = message.typeName
156156
}
157157

158158
is CompositeOperationMessage -> {
159-
parts += message.parts.associate { part ->
160-
when (part) {
159+
message.parts.forEach { child ->
160+
when (child) {
161161
is ElementOperationMessage -> {
162-
// TODO generate wrapper root element and place element part XML within it
163-
TODO("Element parts in composite messages are not supported")
162+
// swap element recreation with 'ref'
163+
// e.g. <element name="foo" ref="tns:foo" />
164+
val refType = QName(child.elementName.namespaceURI, child.elementName.localPart, "ref:${child.elementName.prefix}")
165+
parts[child.elementName.localPart] = refType
164166
}
165167

166168
is TypeOperationMessage -> {
167-
part.partName to part.typeName
169+
parts[child.partName] = child.typeName
168170
}
169171

170172
else -> throw UnsupportedOperationException(
171-
"Unsupported output message part: ${part::class.java.canonicalName}"
173+
"Unsupported output message part: ${child::class.java.canonicalName}"
172174
)
173175
}
174176
}
@@ -203,9 +205,8 @@ class SoapExampleService {
203205
): String {
204206
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)
205207

206-
// TODO should this use the qualified name instead?
207-
val rootElementName = outputRef.elementName.localPart
208-
val elem: SchemaType = sts.documentTypes().find { it.documentElementName.localPart == rootElementName }
208+
val rootElementName = outputRef.elementName
209+
val elem: SchemaType = sts.documentTypes().find { it.documentElementName == rootElementName }
209210
?: throw RuntimeException("Could not find a global element with name \"$rootElementName\"")
210211

211212
return SampleXmlUtil.createSampleForType(elem)

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/util/SchemaGenerator.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ object SchemaGenerator {
5959
if (partType.namespaceURI?.isNotBlank() == true) {
6060
namespaces[partType.prefix] = partType.namespaceURI
6161
}
62+
if (!namespaces.containsKey("xs")) {
63+
namespaces["xs"] ="http://www.w3.org/2001/XMLSchema"
64+
}
6265

6366
val namespacesXml = namespaces.entries.joinToString(separator = "\n") { (prefix, nsUri) ->
6467
"""xmlns:${prefix}="${nsUri}""""
6568
}
6669
val schemaXml = """
6770
<xs:schema elementFormDefault="unqualified" version="1.0"
68-
xmlns:xs="http://www.w3.org/2001/XMLSchema"
6971
${namespacesXml.prependIndent(" ".repeat(11))}
7072
targetNamespace="${partType.namespaceURI}">
7173
@@ -77,22 +79,39 @@ ${namespacesXml.prependIndent(" ".repeat(11))}
7779
return SchemaDocument.Factory.parse(schemaXml)
7880
}
7981

82+
/**
83+
* @param parts map of element name to element qualified type
84+
*/
8085
fun createCompositePartSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
8186
val namespaces = mutableMapOf<String, String>()
8287
if (rootElement.namespaceURI?.isNotBlank() == true) {
8388
namespaces[rootElement.prefix] = rootElement.namespaceURI
8489
}
85-
namespaces += parts.values.associate { it.prefix to it.namespaceURI }
90+
namespaces += parts.values.associate {
91+
val prefix = if (it.prefix.startsWith("ref:")) {
92+
it.prefix.substringAfter("ref:")
93+
} else {
94+
it.prefix
95+
}
96+
prefix to it.namespaceURI
97+
}
98+
if (!namespaces.containsKey("xs")) {
99+
namespaces["xs"] = "http://www.w3.org/2001/XMLSchema"
100+
}
86101

87102
val namespacesXml = namespaces.entries.joinToString(separator = "\n") { (prefix, nsUri) ->
88103
"""xmlns:${prefix}="${nsUri}""""
89104
}
90105
val partsXml = parts.entries.joinToString(separator = "\n") { (partName, partType) ->
91-
"""<xs:element name="$partName" type="${partType.prefix}:${partType.localPart}"/>"""
106+
if (partType.prefix.startsWith("ref:")) {
107+
val refTarget = partType.prefix.substringAfter("ref:")
108+
"""<xs:element ref="${refTarget}:${partType.localPart}"/>"""
109+
} else {
110+
"""<xs:element name="$partName" type="${partType.prefix}:${partType.localPart}"/>"""
111+
}
92112
}
93113
val schemaXml = """
94114
<xs:schema elementFormDefault="unqualified" version="1.0"
95-
xmlns:xs="http://www.w3.org/2001/XMLSchema"
96115
${namespacesXml.prependIndent(" ".repeat(11))}
97116
targetNamespace="${rootElement.namespaceURI}">
98117
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2016-2021.
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+
package io.gatehill.imposter.plugin.soap
44+
45+
import io.gatehill.imposter.plugin.soap.util.SoapUtil
46+
47+
/**
48+
* Tests for [SoapPluginImpl] using WSDL v1 and SOAP 1.1.
49+
*
50+
* @author Pete Cornish
51+
*/
52+
class Wsdl1Soap11CompositeMessageEndToEndTest : AbstractEndToEndTest() {
53+
override val testConfigDirs = listOf("/wsdl1-soap11-composite-message")
54+
override val soapEnvNamespace = SoapUtil.soap11EnvNamespace
55+
override val soapContentType = SoapUtil.soap11ContentType
56+
57+
override val getPetByIdEnv: String
58+
get() = SoapUtil.wrapInEnv(
59+
"""
60+
<getPetById xmlns="urn:com:example:petstore">
61+
<id>3</id>
62+
</getPetById>
63+
""".trim(), soapEnvNamespace
64+
)
65+
66+
override val getPetByNameEnv
67+
get() = SoapUtil.wrapInEnv(
68+
"""
69+
<getPetByName xmlns="urn:com:example:petstore">
70+
<name>Fluffy</name>
71+
</getPetByName>
72+
""".trim(), soapEnvNamespace
73+
)
74+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<env:Envelope xmlns:env="http://www.w3.org/2001/12/soap-envelope">
3+
<env:Header/>
4+
<env:Body>
5+
<getPetByNameResponse xmlns="urn:com:example:petstore">
6+
<pet>
7+
<id>3</id>
8+
<name>Fluffy</name>
9+
</pet>
10+
</getPetByNameResponse>
11+
</env:Body>
12+
</env:Envelope>

0 commit comments

Comments
 (0)