Skip to content

Instantly share code, notes, and snippets.

@libetl
Created November 30, 2023 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save libetl/19d6cb558d9e9dfd5fb97d74e5c128f2 to your computer and use it in GitHub Desktop.
Save libetl/19d6cb558d9e9dfd5fb97d74e5c128f2 to your computer and use it in GitHub Desktop.
soap with spring-webmvc
package com.company.service.mysoapservice.infra.controller
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.inSoapEnvelope
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.`→`
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.`↘`
import com.company.service.mysoapservice.transverse.xml.XmlBuilder.invoke
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestClientException
import org.springframework.web.client.RestTemplate
import org.springframework.ws.server.endpoint.annotation.XPathParam
import java.nio.charset.Charset
import java.util.Base64
import kotlin.text.RegexOption.IGNORE_CASE
@RestController("sample soap-service Endpoint")
@RequestMapping(
value = ["/sampleSoapService"],
produces = [MediaType.ALL_VALUE]
)
class Endpoint() {
@PostMapping(headers = ["SOAPAction=\"http://company.com/sampleSoapService/SomeOperation\""])
internal fun someOperation(
@XPathParam("//ser:name") name: String
): ResponseEntity<String> {
return "Result" {
"Hello $name"
}
.inSoapEnvelope(
"SomeOperationResponse",
rootTagNamespaces = mapOf("xmlns" to "http://company.com/sampleSoapService/"),
bodyNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
headerNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeTagsPrefixes = "",
headerContent = "<></>"
)
}
@PostMapping(headers = ["SOAPAction=\"http://company.com/sampleSoapService/AddTwoNumbers\""])
internal fun addTwoNumbers(
@XPathParam("//ser:firstNumber") firstNumber: String,
@XPathParam("//ser:secondNumber") secondNumber: String
): ResponseEntity<String> {
return "AddTwoNumbersResult" {
"${firstNumber.toInt() + secondNumber}"
}
.inSoapEnvelope(
"AddTwoNumbersResponse",
rootTagNamespaces = mapOf("xmlns" to "http://company.com/sampleSoapService/"),
bodyNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
headerNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeTagsPrefixes = "",
headerContent = "<></>"
)
}
@ExceptionHandler(value = [NumberFormatException::class, IllegalArgumentException::class])
fun handleRestClientException(exception: Exception) =
"faultstring" {
"${exception::class} : ${exception.message}"
}.inSoapEnvelope(
"Fault",
rootTagNamespaces = mapOf("xmlns" to "http://company.com/sampleSoapService/"),
bodyNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
headerNamespaces = mapOf("xmlns" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeTagsPrefixes = "",
headerContent = "<></>"
)
}
package com.company.service.mysoapservice.transverse.xml
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.actualRoot
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.`→`
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.`→?`
import com.company.service.mysoapservice.transverse.xml.ResponseUtils.`↘`
import jakarta.servlet.Filter
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.MethodParameter
import org.springframework.http.HttpInputMessage
import org.springframework.http.HttpOutputMessage
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import java.nio.charset.Charset
@Configuration class NaiveXmlMessageConverterConfig :
WebMvcConfigurer {
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
converters.add(naiveXmlMessageWriter)
super.configureMessageConverters(converters)
}
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(
xPathParamHandlerMethodArgumentResolver
)
super.addArgumentResolvers(resolvers)
}
val naiveXmlMessageWriter =
object : HttpMessageConverter<String> {
override fun canRead(
clazz: Class<*>, mediaType: MediaType?
): Boolean {
return false
}
override fun canWrite(
clazz: Class<*>, mediaType: MediaType?
): Boolean {
return (mediaType?.includes(MediaType.APPLICATION_XML) == true || mediaType?.includes(
MediaType.TEXT_XML
) == true || mediaType?.includes(MediaType.TEXT_PLAIN) == true) && String::class.java.isAssignableFrom(
clazz
)
}
override fun getSupportedMediaTypes(): MutableList<MediaType> {
return mutableListOf(
MediaType.APPLICATION_XML,
MediaType.TEXT_XML,
MediaType.TEXT_PLAIN,
MediaType.ALL
)
}
override fun write(
t: String,
contentType: MediaType?,
outputMessage: HttpOutputMessage
) {
return outputMessage.body.write(t.toByteArray());
}
override fun read(
clazz: Class<out String>,
inputMessage: HttpInputMessage
): String {
return ""
}
}
val xPathParamHandlerMethodArgumentResolver =
object : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean =
parameter.hasParameterAnnotation(XPathParam::class.java) && parameter.parameterType == String::class.java && parameter.getParameterAnnotation(
XPathParam::class.java
)?.value?.startsWith("//") == true
fun access(
map: Map<String, Any?>, accessor: String
): Map<String, Any?> {
val localName =
accessor.split(":").last().lowercase()
val wantedKey = map.filterKeys {
it.split(":").last()
.lowercase() == localName
}.keys.firstOrNull() ?: return mapOf()
return map `→` wantedKey
}
fun read(
map: Map<String, Any?>, accessor: String
): String? {
val localName =
accessor.split(":").last().lowercase()
val wantedKey = map.filterKeys {
it.split(":").last()
.lowercase() == localName
}.keys.firstOrNull() ?: return ""
if ((map[wantedKey] as? Map<*, *>)?.keys?.equals(
setOf("CDATA")
) == true
) return ((map[wantedKey] as? Map<*, *>)?.get(
"CDATA"
) as? String) ?: ""
if (map[wantedKey] is Map<*, *>) return ""
return map `↘` wantedKey
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any {
val root = webRequest.getNativeRequest(
XmlStructuringResponseWrapper::class.java
)?.xml() ?: mapOf()
val parentMap =
root `→?` "Envelope" `→?` "Body"
val map =
parentMap.filterValues { it is Map<*, *> }.values.first() as Map<String, Any?>
val accessors =
parameter.getParameterAnnotation(
XPathParam::class.java
)!!.value.substring(2).split("/")
val mapAccessors = accessors.dropLast(1)
val current =
mapAccessors.foldRight(map) { accessor, current ->
access(current, accessor)
}
return read(current, accessors.last()) ?: ""
}
}
@Bean fun decorateWithXmlStructuringResponseWrapper() =
Filter { p0, p1, p2 ->
p2.doFilter(
XmlStructuringResponseWrapper(p0 as HttpServletRequest),
p1
)
}
internal class XmlStructuringResponseWrapper(request: HttpServletRequest) :
HttpServletRequestWrapper(request) {
private var map: Map<String, Any?> = mapOf()
fun xml() = map
private val namespaceMappingByUrlSuffix =
mapOf<String, (String) -> String>(
"/sampleSoapService" to { "\"http://www.company.com/$it\"" })
private val soapActionHeader
get() = (request as? HttpServletRequest)?.getHeader(
"SOAPAction"
)
override fun getHeader(name: String?): String? {
if (name?.lowercase() == "soapaction" && (
soapActionHeader.isNullOrBlank() ||
soapActionHeader!!.matches(
Regex("\\s*\"\\s*\"\\s*")
))
) {
return namespaceMappingByUrlSuffix[(request as HttpServletRequest).servletPath]!!(
map.actualRoot.key
)
}
return super.getHeader(name)
}
init {
val xmlPayloadAsText =
request.inputStream.readAllBytes()
.toString(Charset.defaultCharset())
.replace(Regex(">\\s*<"), "><")
map = XmlToMap.xmlToMap(
xmlPayloadAsText,
XmlToMap.Options().ignoreNameSpace()
)
}
}
}
package com.company.service.mysoapservice.transverse.xml
import com.company.service.mysoapservice.transverse.xml.XmlBuilder.invoke
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
internal object ResponseUtils {
private fun Map<String, Any?>.keysToLowerCase() =
mapKeys { it.key.lowercase() }
@Suppress("UNCHECKED")
infix fun Map<String, Any?>.`→`(key: String) =
keysToLowerCase().getOrDefault(
key.lowercase(),
mapOf<String, Any?>()
) as Map<String, Any?>
@Suppress("UNCHECKED")
infix fun Map<String, Any?>.`→?`(key: String) =
keysToLowerCase().getOrDefault(
key.lowercase(),
this
) as Map<String, Any?>
@Suppress("UNCHECKED")
infix fun Map<String, Any?>.`↘`(key: String) =
keysToLowerCase().run {
val lowercase = key.lowercase()
if (this[lowercase] == null || this[lowercase] is Map<*, *>) null
else this.getOrDefault(
lowercase,
null
) as String?
}
@Suppress("UNCHECKED")
infix fun Map<String, Any?>.`{`(key: String): List<Map<String, Any?>> =
keysToLowerCase().run {
val lowercase = key.lowercase()
if (this.size == 2 && this.get("arrayoftype") == key) this.get(
"children"
) as List<Map<String, Any?>> else
(this.get(lowercase) as? List<*> ?: this
.get(lowercase)
?.run { listOf(this) }
?: listOf()) as List<Map<String, Any?>>
}
val Map<String, Any?>.actualRoot get() =
(this `→?` "?xml" `→?` "envelope" `→?` "body")
.entries.first { it.value is Map<*, *> }
as Map.Entry<String, Map<String, Any?>>
fun String.inSoapEnvelope(
tag: String,
rootTagNamespaces: Map<String, String> = mapOf(),
headerNamespaces: Map<String, String> = mapOf(),
bodyNamespaces: Map<String, String> = mapOf(),
envelopeNamespaces: Map<String, String> =
mapOf("xmlns:SOAP-ENV" to "http://schemas.xmlsoap.org/soap/envelope/"),
envelopeTagsPrefixes: String = "SOAP-ENV:",
headerContent: String = ""
) =
ResponseEntity(
"${envelopeTagsPrefixes}Envelope"(*envelopeNamespaces
.map { it.key to it.value }
.toTypedArray()) {
"${envelopeTagsPrefixes}Header"(*headerNamespaces
.map { it.key to it.value }
.toTypedArray()) { headerContent } +
"${envelopeTagsPrefixes}Body"(
*bodyNamespaces.entries
.map { it.key to it.value }
.toTypedArray()) {
tag(
*rootTagNamespaces.entries
.map { it.key to it.value }
.toTypedArray()
) {
this@inSoapEnvelope
}
}
},
HttpHeaders().apply {
put("Content-Type", listOf("text/xml"))
},
HttpStatusCode.valueOf(200)
)
}
package com.company.service.mysoapservice.transverse.xml
// this is a builder to avoid writing xml tags as string without indentation.
internal object XmlBuilder {
// Any must have .toString() operation
@Suppress("UNCHECKED_CAST")
operator fun String.invoke(
vararg attrs: Pair<String, Any?>,
nested: StringBuilder.() -> Any?
): String {
val stringBuilder = StringBuilder()
val result = nested(stringBuilder) ?: return ""
val attributesAsString =
attrs
.filter { it.second != null }
.joinToString("") { """ ${it.first}="${it.second}"""" }
if (result == "")
return "<$this${attributesAsString}/>"
if (result == "<></>")
return "<$this${attributesAsString}></$this>"
return "<$this${attributesAsString}>${
when {
result is Iterable<*> && result.all { it is Iterable<*> } ->
stringBuilder.append(
(result as Iterable<Iterable<*>>)
.flatten().joinToString("")
)
(result is Iterable<*>) ->
stringBuilder.append(
result.joinToString(
""
)
)
(result != Unit) ->
stringBuilder.append(result)
else -> ""
}
}</$this>"
}
// tag method is used when using if statements
fun StringBuilder.t(
name: String,
vararg attrs: Pair<String, Any>,
nested: StringBuilder.() -> Any
): StringBuilder {
return this.append(name(*attrs) { nested(this) })
}
operator fun Map<String, Any?>.invoke() =
"""{${entries.joinToString(",") { "<${it.key}>${it.value}</${it.key}>" }}}"""
}
package com.company.service.mysoapservice.transverse.xml
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class XPathParam(val value: String)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment