Created
November 30, 2023 20:34
-
-
Save libetl/19d6cb558d9e9dfd5fb97d74e5c128f2 to your computer and use it in GitHub Desktop.
soap with spring-webmvc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "<></>" | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}>" }}}""" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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