Skip to content

Commit

Permalink
feat(soap): supports more combinations of binding style and messages.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Apr 19, 2024
1 parent 93acbd6 commit 60a628f
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ class SoapPluginImpl @Inject constructor(
LOGGER.warn("Unable to find a matching binding operation using SOAPAction or SOAP request body")
return@build completedUnitFuture()
}
check(operation.style.equals("document", ignoreCase = true)
|| operation.style.equals("rpc", ignoreCase = true)) {
check(operation.style.equals(SoapUtil.OPERATION_STYLE_DOCUMENT, ignoreCase = true)
|| operation.style.equals(SoapUtil.OPERATION_STYLE_RPC, ignoreCase = true)) {
"Only document and RPC style SOAP bindings are supported"
}

Expand Down Expand Up @@ -250,7 +250,7 @@ class SoapPluginImpl @Inject constructor(
parser.schemas,
wsdlDir,
service,
operation.outputRef,
operation,
bodyHolder
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@
package io.gatehill.imposter.plugin.soap.service

import io.gatehill.imposter.http.HttpExchange
import io.gatehill.imposter.plugin.soap.model.CompositeOperationMessage
import io.gatehill.imposter.plugin.soap.model.ElementOperationMessage
import io.gatehill.imposter.plugin.soap.model.MessageBodyHolder
import io.gatehill.imposter.plugin.soap.model.OperationMessage
import io.gatehill.imposter.plugin.soap.model.ParsedRawBody
import io.gatehill.imposter.plugin.soap.model.ParsedSoapMessage
import io.gatehill.imposter.plugin.soap.model.TypeOperationMessage
import io.gatehill.imposter.plugin.soap.model.WsdlOperation
import io.gatehill.imposter.plugin.soap.model.WsdlService
import io.gatehill.imposter.plugin.soap.parser.WsdlRelativeXsdEntityResolver
import io.gatehill.imposter.plugin.soap.util.SchemaGenerator
Expand Down Expand Up @@ -82,57 +83,147 @@ class SoapExampleService {
schemas: Array<SchemaDocument>,
wsdlDir: File,
service: WsdlService,
outputMessage: OperationMessage,
operation: WsdlOperation,
bodyHolder: MessageBodyHolder,
): Boolean {
logger.debug("Generating example for {}", outputMessage)
val example = generateInstanceFromSchemas(schemas, wsdlDir, service, outputMessage)
logger.debug("Generating example for {}", operation.outputRef)
val example = when (operation.style) {
SoapUtil.OPERATION_STYLE_DOCUMENT -> generateDocumentResponse(operation, wsdlDir, schemas)
SoapUtil.OPERATION_STYLE_RPC -> generateRpcResponse(service, operation, wsdlDir, schemas)
else -> throw UnsupportedOperationException("Unsupported operation style: ${operation.style}")
}
transmitExample(httpExchange, example, bodyHolder)
return true
}

private fun generateInstanceFromSchemas(
schemas: Array<SchemaDocument>,
private fun generateDocumentResponse(
operation: WsdlOperation,
wsdlDir: File,
service: WsdlService,
message: OperationMessage,
schemas: Array<SchemaDocument>,
): String {
when (message) {
when (val message = operation.outputRef) {
is ElementOperationMessage -> {
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)

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

return SampleXmlUtil.createSampleForType(elem)
return generateElementExample(wsdlDir, schemas, message)
}

is TypeOperationMessage -> {
// by convention, the suffix 'Response' is added to the operation name
val elementName = message.operationName + "Response"
val rootElement = if (service.targetNamespace?.isNotBlank() == true) {
QName(service.targetNamespace, elementName, "tns")
} else {
QName(elementName)
return generateTypeExample(wsdlDir, schemas, message)
}

is CompositeOperationMessage -> {
val partXmls = message.parts.map { part ->
val partXml = when (part) {
is ElementOperationMessage -> {
generateElementExample(wsdlDir, schemas, part)
}

is TypeOperationMessage -> {
generateTypeExample(wsdlDir, schemas, part)
}

else -> throw UnsupportedOperationException(
"Unsupported output message part: ${part::class.java.canonicalName}"
)
}
logger.trace("Generated document part XML: {}", partXml)
partXml
}

val parts = mutableMapOf<String, QName>()
// no root element in document style
return partXmls.joinToString("\n")
}

else -> throw UnsupportedOperationException("Unsupported output message: ${operation.outputRef}")
}
}

private fun generateRpcResponse(
service: WsdlService,
operation: WsdlOperation,
wsdlDir: File,
schemas: Array<SchemaDocument>,
): String {
val parts = mutableMapOf<String, QName>()

when (val message = operation.outputRef) {
is ElementOperationMessage -> {
// TODO generate wrapper root element and place element part XML within it
TODO("Element parts in RPC bindings are not supported")
}

is TypeOperationMessage -> {
parts[message.partName] = message.typeName
}

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

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

return SampleXmlUtil.createSampleForType(elem)
else -> throw UnsupportedOperationException(
"Unsupported output message part: ${part::class.java.canonicalName}"
)
}
}
}

// TODO support CompositeOperationMessage
else -> throw UnsupportedOperationException("Unsupported output message: ${message::class.java.canonicalName}")
else -> throw UnsupportedOperationException(
"Unsupported output message: $message"
)
}

// by convention, the suffix 'Response' is added to the operation name
val rootElementName = operation.name + "Response"
val rootElement = if (service.targetNamespace?.isNotBlank() == true) {
QName(service.targetNamespace, rootElementName, "tns")
} else {
QName(rootElementName)
}

val elementSchema = SchemaGenerator.createCompositePartSchema(rootElement, parts)
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)

val elem = sts.documentTypes().find { it.documentElementName == rootElement }
?: throw RuntimeException("Could not find a generated element with name \"$rootElement\"")

return SampleXmlUtil.createSampleForType(elem)
}

private fun generateElementExample(
wsdlDir: File,
schemas: Array<SchemaDocument>,
outputRef: ElementOperationMessage,
): String {
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas)

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

return SampleXmlUtil.createSampleForType(elem)
}

private fun generateTypeExample(
wsdlDir: File,
schemas: Array<SchemaDocument>,
message: TypeOperationMessage,
): String {
val elementName = message.partName
val elementSchema = SchemaGenerator.createSinglePartSchema(elementName, message.typeName)
val sts: SchemaTypeSystem = buildSchemaTypeSystem(wsdlDir, schemas + elementSchema)

val elem = sts.documentTypes().find { it.documentElementName.localPart == elementName }
?: throw RuntimeException("Could not find a generated element with name \"$elementName\"")

return SampleXmlUtil.createSampleForType(elem)
}

private fun buildSchemaTypeSystem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,30 @@ import javax.xml.namespace.QName
object SchemaGenerator {
private val logger: Logger = LogManager.getLogger(SchemaGenerator::class.java)

fun createElementSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
fun createSinglePartSchema(elementName: String, partType: QName): SchemaDocument {
val namespaces = mutableMapOf<String, String>()
if (partType.namespaceURI?.isNotBlank() == true) {
namespaces[partType.prefix] = partType.namespaceURI
}

val namespacesXml = namespaces.entries.joinToString(separator = "\n") { (prefix, nsUri) ->
"""xmlns:${prefix}="${nsUri}""""
}
val schemaXml = """
<xs:schema elementFormDefault="unqualified" version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
${namespacesXml.prependIndent(" ".repeat(11))}
targetNamespace="${partType.namespaceURI}">
<xs:element name="$elementName" type="${partType.prefix}:${partType.localPart}" />
</xs:schema>
""".trim()

logger.trace("Generated element schema:\n{}", schemaXml)
return SchemaDocument.Factory.parse(schemaXml)
}

fun createCompositePartSchema(rootElement: QName, parts: Map<String, QName>): SchemaDocument {
val namespaces = mutableMapOf<String, String>()
if (rootElement.namespaceURI?.isNotBlank() == true) {
namespaces[rootElement.prefix] = rootElement.namespaceURI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import org.jdom2.Namespace
import org.jdom2.input.SAXBuilder

object SoapUtil {
const val OPERATION_STYLE_DOCUMENT = "document"
const val OPERATION_STYLE_RPC = "rpc"
const val textXmlContentType = "text/xml"
const val soap11ContentType = textXmlContentType
const val soap12ContentType = "application/soap+xml"
Expand Down

0 comments on commit 60a628f

Please sign in to comment.