Skip to content

Commit 37a9836

Browse files
committed
feat(soap): adds support for messages with type attribute.
1 parent e276046 commit 37a9836

23 files changed

+724
-63
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ class SoapPluginImpl @Inject constructor(
185185
LOGGER.warn("Unable to find a matching binding operation using SOAPAction or SOAP request body")
186186
return@build completedUnitFuture()
187187
}
188-
check(operation.style.equals("document", ignoreCase = true)) {
189-
"Only document SOAP bindings are supported"
190-
}
188+
// check(operation.style.equals("document", ignoreCase = true)) {
189+
// "Only document SOAP bindings are supported"
190+
// }
191191

192192
LOGGER.debug("Matched operation: ${operation.name} in binding ${binding.name}")
193193
return@build handle(config, parser, binding, operation, httpExchange, bodyHolder, soapAction)
@@ -224,8 +224,8 @@ class SoapPluginImpl @Inject constructor(
224224
val response = httpExchange.response
225225
.setStatusCode(responseBehaviour.statusCode)
226226

227-
operation.outputElementRef?.let {
228-
LOGGER.trace("Using output schema type: ${operation.outputElementRef}")
227+
operation.outputRef?.let {
228+
LOGGER.trace("Using output schema type: ${operation.outputRef}")
229229

230230
if (!responseBehaviour.responseHeaders.containsKey(HttpUtil.CONTENT_TYPE)) {
231231
responseBehaviour.responseHeaders[HttpUtil.CONTENT_TYPE] = when (bodyHolder) {
@@ -245,7 +245,7 @@ class SoapPluginImpl @Inject constructor(
245245
httpExchange,
246246
parser.schemas,
247247
wsdlDir,
248-
operation.outputElementRef,
248+
operation.outputRef,
249249
bodyHolder
250250
)
251251
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import io.gatehill.imposter.plugin.config.PluginConfig
5252
import io.gatehill.imposter.plugin.soap.config.SoapPluginConfig
5353
import io.gatehill.imposter.plugin.soap.config.SoapPluginResourceConfig
5454
import io.gatehill.imposter.plugin.soap.model.BindingType
55+
import io.gatehill.imposter.plugin.soap.model.ElementOperationMessage
5556
import io.gatehill.imposter.plugin.soap.model.MessageBodyHolder
5657
import io.gatehill.imposter.plugin.soap.model.WsdlBinding
5758
import io.gatehill.imposter.plugin.soap.model.WsdlOperation
@@ -114,7 +115,7 @@ class SoapResourceMatcher(
114115
}
115116

116117
private fun matchBinding(
117-
resourceConfig: SoapPluginResourceConfig
118+
resourceConfig: SoapPluginResourceConfig,
118119
): ResourceMatchResult {
119120
val matchDescription = "SOAP binding"
120121
return resourceConfig.binding?.let {
@@ -201,8 +202,9 @@ class SoapResourceMatcher(
201202
}
202203

203204
val matchedOps = binding.operations.filter { op ->
204-
op.inputElementRef?.namespaceURI == bodyRootElement.namespaceURI &&
205-
op.inputElementRef?.localPart == bodyRootElement.name
205+
op.inputRef is ElementOperationMessage &&
206+
op.inputRef.elementName.namespaceURI == bodyRootElement.namespaceURI &&
207+
op.inputRef.elementName.localPart == bodyRootElement.name
206208
}
207209
if (LOGGER.isTraceEnabled) {
208210
LOGGER.trace(
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.model
45+
46+
import javax.xml.namespace.QName
47+
48+
/**
49+
* Represents an operation message. In WSDL 1.1 this is an individual
50+
* `part` element within a `message`. In WSDL 2.0 this is an `input` or `output`
51+
* element within an `operation`.
52+
*/
53+
abstract class OperationMessage(
54+
val operationName: String,
55+
)
56+
57+
/**
58+
* Refers to an XML schema `element`.
59+
*/
60+
class ElementOperationMessage(
61+
operationName: String,
62+
val elementName: QName,
63+
) : OperationMessage(
64+
operationName
65+
)
66+
67+
/**
68+
* Message parts specifying an XML schema `type` are supported
69+
* in WSDL 1.1 but not in WSDL 2.0.
70+
*/
71+
class TypeOperationMessage(
72+
operationName: String,
73+
val typeName: QName,
74+
): OperationMessage(
75+
operationName
76+
)
77+
78+
/**
79+
* In WSDL 1.1, messages can define multiple parts.
80+
*/
81+
class CompositeOperationMessage(
82+
operationName: String,
83+
84+
/**
85+
* Maps the `part` name to an operation message.
86+
*/
87+
val parts: Map<String, OperationMessage>
88+
): OperationMessage(
89+
operationName
90+
)

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
package io.gatehill.imposter.plugin.soap.model
4545

4646
import java.net.URI
47-
import javax.xml.namespace.QName
4847

4948
data class WsdlService(
5049
val name: String,
@@ -79,6 +78,6 @@ data class WsdlOperation(
7978
val name: String,
8079
val soapAction: String?,
8180
val style: String?,
82-
val inputElementRef: QName?,
83-
val outputElementRef: QName?,
81+
val inputRef: OperationMessage?,
82+
val outputRef: OperationMessage?,
8483
)

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

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ abstract class AbstractWsdlParser(
7373

7474
protected val xsd: SchemaTypeSystem by lazy { buildXsdFromSchemas() }
7575

76+
private val unionTypeSystem by lazy { XmlBeans.typeLoaderUnion(XmlBeans.getBuiltinTypeSystem(), xsd) }
77+
7678
private fun discoverSchemas(): Array<SchemaDocument> {
7779
val schemas = mutableListOf<SchemaDocument>()
7880
schemas += findEmbeddedTypesSchemas()
@@ -153,15 +155,48 @@ abstract class AbstractWsdlParser(
153155
* Attempt to resolve the element with the given, optionally qualified, name
154156
* from within the XSD.
155157
*/
156-
protected fun resolveElementFromXsd(elementName: String): QName? {
157-
val localPart = SoapUtil.getLocalPart(elementName)
158-
158+
protected fun resolveElementFromXsd(elementQName: QName): QName? {
159+
// TODO should this use xsd.findElement(elementQName).name instead?
160+
// TODO should this use QName.equals instead?
161+
// TODO should this be filtered on unqualified elements?
159162
// the top level element from the XSD
160-
val matchingTypeElement: QName? =
161-
// TODO should this be filtered on unqualified elements?
162-
xsd.documentTypes().find { it.documentElementName.localPart == localPart }?.documentElementName
163+
val matchingElement: QName? =
164+
xsd.documentTypes().find { it.documentElementName.localPart == elementQName.localPart }?.documentElementName
165+
166+
logger.trace("Resolved element name {} to qualified type: {}", elementQName, matchingElement)
167+
return matchingElement
168+
}
169+
170+
/**
171+
* Attempt to resolve the type with the given, optionally qualified, name
172+
* from within the XSD.
173+
*/
174+
protected fun resolveTypeFromXsd(typeQName: QName): QName? {
175+
val matchingType = unionTypeSystem.findType(typeQName)?.name
176+
?: return null
177+
178+
logger.trace("Resolved type name {} to qualified type: {}", typeQName, matchingType)
179+
return matchingType
180+
}
181+
182+
protected fun getAttributeValueAsQName(element: Element, attributeName: String): QName? {
183+
val attr = element.getAttribute(attributeName)
184+
?: return null
163185

164-
logger.trace("Resolved element name {} to qualified type: {}", elementName, matchingTypeElement)
165-
return matchingTypeElement
186+
val attrValue = attr.value
187+
?: return null
188+
189+
val valueParts = attrValue.split(':')
190+
if (valueParts.size == 1) {
191+
// unqualified name
192+
return QName(valueParts[0])
193+
} else {
194+
val prefix = valueParts[0]
195+
val localName = valueParts[1]
196+
val ns = attr.namespacesInScope.find { it.prefix == prefix }?.uri
197+
?: throw IllegalStateException("No namespace in scope with prefix '$prefix' for attribute '$attributeName' in element: $element")
198+
199+
return QName(ns, localName, prefix)
200+
}
166201
}
167202
}

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
package io.gatehill.imposter.plugin.soap.parser
4545

4646
import io.gatehill.imposter.plugin.soap.model.BindingType
47+
import io.gatehill.imposter.plugin.soap.model.CompositeOperationMessage
48+
import io.gatehill.imposter.plugin.soap.model.ElementOperationMessage
49+
import io.gatehill.imposter.plugin.soap.model.OperationMessage
50+
import io.gatehill.imposter.plugin.soap.model.TypeOperationMessage
4751
import io.gatehill.imposter.plugin.soap.model.WsdlBinding
4852
import io.gatehill.imposter.plugin.soap.model.WsdlEndpoint
4953
import io.gatehill.imposter.plugin.soap.model.WsdlInterface
@@ -57,7 +61,6 @@ import org.jdom2.Namespace
5761
import org.xml.sax.EntityResolver
5862
import java.io.File
5963
import java.net.URI
60-
import javax.xml.namespace.QName
6164

6265
/**
6366
* WSDL 1.x parser.
@@ -170,10 +173,10 @@ class Wsdl1Parser(
170173
name = operationName
171174
) ?: throw IllegalStateException("No portType operation found for portType: $portTypeName and operation: $operationName")
172175

173-
val input = getMessagePartElementName(portTypeOperation, "./wsdl:input")
176+
val input = getMessage(operationName, portTypeOperation, "./wsdl:input")
174177
?: throw IllegalStateException("No input found for portType operation: $operationName")
175178

176-
val output = getMessagePartElementName(portTypeOperation, "./wsdl:output")
179+
val output = getMessage(operationName, portTypeOperation, "./wsdl:output")
177180
?: throw IllegalStateException("No output found for portType operation: $operationName")
178181

179182
val style = soapOperation.getAttributeValue("style") ?: run {
@@ -188,8 +191,8 @@ class Wsdl1Parser(
188191
name = bindingOperation.getAttributeValue("name"),
189192
soapAction = soapOperation.getAttributeValue("soapAction"),
190193
style = style,
191-
inputElementRef = input,
192-
outputElementRef = output,
194+
inputRef = input,
195+
outputRef = output,
193196
)
194197
}
195198

@@ -211,7 +214,7 @@ class Wsdl1Parser(
211214
* Extract the WSDL message part element attribute, then attempt
212215
* to resolve it from within the XSD.
213216
*/
214-
private fun getMessagePartElementName(context: Element, expression: String): QName? {
217+
private fun getMessage(operationName: String, context: Element, expression: String): OperationMessage? {
215218
val inputOrOutputNode = selectSingleNode(context, expression)
216219
?: throw IllegalStateException("No input or output found for: $expression")
217220

@@ -221,11 +224,33 @@ class Wsdl1Parser(
221224
val messageName = msgAttr.let { SoapUtil.getLocalPart(msgAttr) }
222225

223226
// look up message
224-
val messagePart = selectSingleNode(document, "/wsdl:definitions/wsdl:message[@name='$messageName']/wsdl:part")
227+
val message = selectSingleNode(document, "/wsdl:definitions/wsdl:message[@name='$messageName']")
225228
?: throw IllegalStateException("Message $msgAttr not found")
226229

227-
val elementName = messagePart.getAttributeValue("element")
228-
return resolveElementFromXsd(elementName)
230+
// look up message parts
231+
val messageParts = selectNodes(message, "./wsdl:part")
232+
233+
val parts: Map<String, OperationMessage> = messageParts.associate { messagePart ->
234+
val partName = messagePart.getAttributeValue("name")
235+
236+
// WSDL 1.1 allows message parts to refer to XML schema types
237+
// directly as well as referring to elements.
238+
val part = getAttributeValueAsQName(messagePart, "element")?.let { elementQName ->
239+
resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(operationName, it) }
240+
} ?: getAttributeValueAsQName(messagePart, "type")?.let { typeQName ->
241+
resolveTypeFromXsd(typeQName)?.let { TypeOperationMessage(operationName, it) }
242+
} ?: throw IllegalStateException(
243+
"Invalid 'element' or 'type' attribute for message: $messageName"
244+
)
245+
246+
partName to part
247+
}
248+
249+
return when (parts.size) {
250+
0 -> return null
251+
1 -> parts.values.first()
252+
else -> CompositeOperationMessage(operationName, parts)
253+
}
229254
}
230255

231256
override val xPathNamespaces = listOf(

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
package io.gatehill.imposter.plugin.soap.parser
4545

4646
import io.gatehill.imposter.plugin.soap.model.BindingType
47+
import io.gatehill.imposter.plugin.soap.model.ElementOperationMessage
48+
import io.gatehill.imposter.plugin.soap.model.OperationMessage
4749
import io.gatehill.imposter.plugin.soap.model.WsdlBinding
4850
import io.gatehill.imposter.plugin.soap.model.WsdlEndpoint
4951
import io.gatehill.imposter.plugin.soap.model.WsdlInterface
@@ -56,7 +58,6 @@ import org.jdom2.Namespace
5658
import org.xml.sax.EntityResolver
5759
import java.io.File
5860
import java.net.URI
59-
import javax.xml.namespace.QName
6061

6162
/**
6263
* WDSL 2.0 parser.
@@ -145,7 +146,7 @@ class Wsdl2Parser(
145146
expressionTemplate = "./wsdl:operation[@name='%s']",
146147
name = operationName
147148
)
148-
return operation?.let { parseOperation(operation) }
149+
return operation?.let { parseOperation(operationName, operation) }
149150
}
150151

151152
private fun getInterfaceNode(interfaceName: String): Element? {
@@ -156,32 +157,38 @@ class Wsdl2Parser(
156157
)
157158
}
158159

159-
private fun parseOperation(operation: Element): WsdlOperation {
160+
private fun parseOperation(operationName: String, operation: Element): WsdlOperation {
160161
val soapOperation = selectSingleNode(operation, "./soap:operation") ?: throw IllegalStateException(
161-
"Unable to find soap:operation for operation ${operation.getAttributeValue("name")}"
162+
"Unable to find soap:operation for operation ${operationName}"
162163
)
163-
val input = getMessagePartElementName(operation, "./wsdl:input")
164-
val output = getMessagePartElementName(operation, "./wsdl:output")
164+
val input = getMessage(operationName, operation, "./wsdl:input")
165+
val output = getMessage(operationName, operation, "./wsdl:output")
165166

166167
return WsdlOperation(
167168
name = operation.getAttributeValue("name"),
168169
soapAction = soapOperation.getAttributeValue("soapAction"),
169170
style = soapOperation.getAttributeValue("style"),
170-
inputElementRef = input,
171-
outputElementRef = output,
171+
inputRef = input,
172+
outputRef = output,
172173
)
173174
}
174175

175176
/**
176177
* Extract the WSDL message part element attribute, then attempt
177178
* to resolve it from within the XSD.
178179
*/
179-
private fun getMessagePartElementName(context: Element, expression: String): QName? {
180+
private fun getMessage(operationName: String, context: Element, expression: String): OperationMessage? {
180181
val inputOrOutputNode = selectSingleNode(context, expression) ?: throw IllegalStateException(
181182
"Unable to find message part: $expression"
182183
)
183-
val elementName = inputOrOutputNode.getAttributeValue("element")
184-
return resolveElementFromXsd(elementName)
184+
185+
// WSDL 2.0 doesn't allow operation messages to refer to XML schema types
186+
// directly - instead an element must be used.
187+
getAttributeValueAsQName(inputOrOutputNode, "element")?.let { elementQName ->
188+
return resolveElementFromXsd(elementQName)?.let { ElementOperationMessage(operationName, it) }
189+
} ?: throw IllegalStateException(
190+
"Invalid 'element' attribute for message input/output: ${inputOrOutputNode.name}"
191+
)
185192
}
186193

187194
override val xPathNamespaces = listOf(

0 commit comments

Comments
 (0)