Skip to content

Commit cac9e91

Browse files
committed
feat(soap): supports more combinations of binding style and messages.
1 parent 93acbd6 commit cac9e91

File tree

4 files changed

+151
-35
lines changed

4 files changed

+151
-35
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ class SoapPluginImpl @Inject constructor(
187187
LOGGER.warn("Unable to find a matching binding operation using SOAPAction or SOAP request body")
188188
return@build completedUnitFuture()
189189
}
190-
check(operation.style.equals("document", ignoreCase = true)
191-
|| operation.style.equals("rpc", ignoreCase = true)) {
190+
check(operation.style.equals(SoapUtil.OPERATION_STYLE_DOCUMENT, ignoreCase = true)
191+
|| operation.style.equals(SoapUtil.OPERATION_STYLE_RPC, ignoreCase = true)) {
192192
"Only document and RPC style SOAP bindings are supported"
193193
}
194194

@@ -250,7 +250,7 @@ class SoapPluginImpl @Inject constructor(
250250
parser.schemas,
251251
wsdlDir,
252252
service,
253-
operation.outputRef,
253+
operation,
254254
bodyHolder
255255
)
256256
}

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

Lines changed: 122 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@
4444
package io.gatehill.imposter.plugin.soap.service
4545

4646
import io.gatehill.imposter.http.HttpExchange
47+
import io.gatehill.imposter.plugin.soap.model.CompositeOperationMessage
4748
import io.gatehill.imposter.plugin.soap.model.ElementOperationMessage
4849
import io.gatehill.imposter.plugin.soap.model.MessageBodyHolder
49-
import io.gatehill.imposter.plugin.soap.model.OperationMessage
5050
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
53+
import io.gatehill.imposter.plugin.soap.model.WsdlOperation
5354
import io.gatehill.imposter.plugin.soap.model.WsdlService
5455
import io.gatehill.imposter.plugin.soap.parser.WsdlRelativeXsdEntityResolver
5556
import io.gatehill.imposter.plugin.soap.util.SchemaGenerator
@@ -82,57 +83,147 @@ class SoapExampleService {
8283
schemas: Array<SchemaDocument>,
8384
wsdlDir: File,
8485
service: WsdlService,
85-
outputMessage: OperationMessage,
86+
operation: WsdlOperation,
8687
bodyHolder: MessageBodyHolder,
8788
): Boolean {
88-
logger.debug("Generating example for {}", outputMessage)
89-
val example = generateInstanceFromSchemas(schemas, wsdlDir, service, outputMessage)
89+
logger.debug("Generating example for {}", operation.outputRef)
90+
val example = when (operation.style) {
91+
SoapUtil.OPERATION_STYLE_DOCUMENT -> generateDocumentResponse(operation, wsdlDir, schemas)
92+
SoapUtil.OPERATION_STYLE_RPC -> generateRpcResponse(service, operation, wsdlDir, schemas)
93+
else -> throw UnsupportedOperationException("Unsupported operation style: ${operation.style}")
94+
}
9095
transmitExample(httpExchange, example, bodyHolder)
9196
return true
9297
}
9398

94-
private fun generateInstanceFromSchemas(
95-
schemas: Array<SchemaDocument>,
99+
private fun generateDocumentResponse(
100+
operation: WsdlOperation,
96101
wsdlDir: File,
97-
service: WsdlService,
98-
message: OperationMessage,
102+
schemas: Array<SchemaDocument>,
99103
): String {
100-
when (message) {
104+
when (val message = operation.outputRef) {
101105
is ElementOperationMessage -> {
102-
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)
103-
104-
// TODO should this use the qualified name instead?
105-
val rootElementName = message.elementName.localPart
106-
val elem: SchemaType = sts.documentTypes().find { it.documentElementName.localPart == rootElementName }
107-
?: throw RuntimeException("Could not find a global element with name \"$rootElementName\"")
108-
109-
return SampleXmlUtil.createSampleForType(elem)
106+
return generateElementExample(wsdlDir, schemas, message)
110107
}
111108

112109
is TypeOperationMessage -> {
113-
// by convention, the suffix 'Response' is added to the operation name
114-
val elementName = message.operationName + "Response"
115-
val rootElement = if (service.targetNamespace?.isNotBlank() == true) {
116-
QName(service.targetNamespace, elementName, "tns")
117-
} else {
118-
QName(elementName)
110+
return generateTypeExample(wsdlDir, schemas, message)
111+
}
112+
113+
is CompositeOperationMessage -> {
114+
val partXmls = message.parts.map { part ->
115+
val partXml = when (part) {
116+
is ElementOperationMessage -> {
117+
generateElementExample(wsdlDir, schemas, part)
118+
}
119+
120+
is TypeOperationMessage -> {
121+
generateTypeExample(wsdlDir, schemas, part)
122+
}
123+
124+
else -> throw UnsupportedOperationException(
125+
"Unsupported output message part: ${part::class.java.canonicalName}"
126+
)
127+
}
128+
logger.trace("Generated document part XML: {}", partXml)
129+
partXml
119130
}
120131

121-
val parts = mutableMapOf<String, QName>()
132+
// no root element in document style
133+
return partXmls.joinToString("\n")
134+
}
135+
136+
else -> throw UnsupportedOperationException("Unsupported output message: ${operation.outputRef}")
137+
}
138+
}
139+
140+
private fun generateRpcResponse(
141+
service: WsdlService,
142+
operation: WsdlOperation,
143+
wsdlDir: File,
144+
schemas: Array<SchemaDocument>,
145+
): String {
146+
// by convention, the suffix 'Response' is added to the operation name
147+
val rootElementName = operation.name + "Response"
148+
val rootElement = if (service.targetNamespace?.isNotBlank() == true) {
149+
QName(service.targetNamespace, rootElementName, "tns")
150+
} else {
151+
QName(rootElementName)
152+
}
153+
154+
val parts = mutableMapOf<String, QName>()
155+
156+
when (val message = operation.outputRef) {
157+
is ElementOperationMessage -> {
158+
// TODO generate wrapper root element and place element part XML within it
159+
TODO("Element parts in RPC bindings are not supported")
160+
}
161+
162+
is TypeOperationMessage -> {
122163
parts[message.partName] = message.typeName
164+
}
123165

124-
val elementSchema = SchemaGenerator.createElementSchema(rootElement, parts)
125-
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)
166+
is CompositeOperationMessage -> {
167+
parts += message.parts.associate { part ->
168+
when (part) {
169+
is ElementOperationMessage -> {
170+
// TODO generate wrapper root element and place element part XML within it
171+
TODO("Element parts in composite messages are not supported")
172+
}
126173

127-
val elem = sts.documentTypes().find { it.documentElementName == rootElement }
128-
?: throw RuntimeException("Could not find a generated element with name \"$rootElement\"")
174+
is TypeOperationMessage -> {
175+
part.partName to part.typeName
176+
}
129177

130-
return SampleXmlUtil.createSampleForType(elem)
178+
else -> throw UnsupportedOperationException(
179+
"Unsupported output message part: ${part::class.java.canonicalName}"
180+
)
181+
}
182+
}
131183
}
132184

133-
// TODO support CompositeOperationMessage
134-
else -> throw UnsupportedOperationException("Unsupported output message: ${message::class.java.canonicalName}")
185+
else -> throw UnsupportedOperationException(
186+
"Unsupported output message: $message"
187+
)
135188
}
189+
190+
val elementSchema = SchemaGenerator.createCompositePartSchema(rootElement, parts)
191+
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)
192+
193+
val elem = sts.documentTypes().find { it.documentElementName == rootElement }
194+
?: throw RuntimeException("Could not find a generated element with name \"$rootElement\"")
195+
196+
return SampleXmlUtil.createSampleForType(elem)
197+
}
198+
199+
private fun generateElementExample(
200+
wsdlDir: File,
201+
schemas: Array<SchemaDocument>,
202+
outputRef: ElementOperationMessage,
203+
): String {
204+
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)
205+
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 }
209+
?: throw RuntimeException("Could not find a global element with name \"$rootElementName\"")
210+
211+
return SampleXmlUtil.createSampleForType(elem)
212+
}
213+
214+
private fun generateTypeExample(
215+
wsdlDir: File,
216+
schemas: Array<SchemaDocument>,
217+
message: TypeOperationMessage,
218+
): String {
219+
val elementName = message.partName
220+
val elementSchema = SchemaGenerator.createSinglePartSchema(elementName, message.typeName)
221+
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)
222+
223+
val elem = sts.documentTypes().find { it.documentElementName.localPart == elementName }
224+
?: throw RuntimeException("Could not find a generated element with name \"$elementName\"")
225+
226+
return SampleXmlUtil.createSampleForType(elem)
136227
}
137228

138229
private fun buildSchemaTypeSystem(

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,30 @@ import javax.xml.namespace.QName
5454
object SchemaGenerator {
5555
private val logger: Logger = LogManager.getLogger(SchemaGenerator::class.java)
5656

57-
fun createElementSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
57+
fun createSinglePartSchema(elementName: String, partType: QName): SchemaDocument {
58+
val namespaces = mutableMapOf<String, String>()
59+
if (partType.namespaceURI?.isNotBlank() == true) {
60+
namespaces[partType.prefix] = partType.namespaceURI
61+
}
62+
63+
val namespacesXml = namespaces.entries.joinToString(separator = "\n") { (prefix, nsUri) ->
64+
"""xmlns:${prefix}="${nsUri}""""
65+
}
66+
val schemaXml = """
67+
<xs:schema elementFormDefault="unqualified" version="1.0"
68+
xmlns:xs="http://www.w3.org/2001/XMLSchema"
69+
${namespacesXml.prependIndent(" ".repeat(11))}
70+
targetNamespace="${partType.namespaceURI}">
71+
72+
<xs:element name="$elementName" type="${partType.prefix}:${partType.localPart}" />
73+
</xs:schema>
74+
""".trim()
75+
76+
logger.trace("Generated element schema:\n{}", schemaXml)
77+
return SchemaDocument.Factory.parse(schemaXml)
78+
}
79+
80+
fun createCompositePartSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
5881
val namespaces = mutableMapOf<String, String>()
5982
if (rootElement.namespaceURI?.isNotBlank() == true) {
6083
namespaces[rootElement.prefix] = rootElement.namespaceURI

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import org.jdom2.Namespace
5454
import org.jdom2.input.SAXBuilder
5555

5656
object SoapUtil {
57+
const val OPERATION_STYLE_DOCUMENT = "document"
58+
const val OPERATION_STYLE_RPC = "rpc"
5759
const val textXmlContentType = "text/xml"
5860
const val soap11ContentType = textXmlContentType
5961
const val soap12ContentType = "application/soap+xml"

0 commit comments

Comments
 (0)