Skip to content

Commit 488a8ca

Browse files
committed
Implement /convert endpoint for xml/json conversion (#165)
1 parent ddb9920 commit 488a8ca

File tree

12 files changed

+517
-12
lines changed

12 files changed

+517
-12
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ kotlin {
101101
implementation("io.ktor:ktor-events:${property("ktorVersion")}")
102102

103103
implementation("io.ktor:ktor-serialization-kotlinx-json:${property("ktorVersion")}")
104+
implementation("io.ktor:ktor-serialization-kotlinx-xml:${property("ktorVersion")}")
104105

105106
implementation("io.ktor:ktor-serialization-gson:${property("ktorVersion")}")
106107
implementation("io.ktor:ktor-serialization-jackson:${property("ktorVersion")}")

src/commonMain/kotlin/constants/Endpoints.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package constants
22

33
const val VALIDATION_ENDPOINT = "validate"
4+
const val CONVERSION_ENDPOINT = "convert"
45
const val VALIDATOR_VERSION_ENDPOINT = "validator/version"
56
const val CONTEXT_ENDPOINT = "context"
67
const val IG_ENDPOINT = "ig"

src/commonMain/resources/static-content/openapi.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,76 @@ paths:
6161
title: validatorVersionOK
6262
type: string
6363
example: "5.6.39"
64+
/convert:
65+
post:
66+
tags:
67+
- Convert a Resource
68+
description: "Converts a resource."
69+
operationId: ConvertAResource
70+
produces:
71+
- application/json
72+
- application/xml
73+
- application/fhir+json
74+
- application/fhir+xml
75+
requestBody:
76+
required: true
77+
content:
78+
application/json:
79+
schema:
80+
type: object
81+
application/fhir+json:
82+
schema:
83+
type: object
84+
application/xml:
85+
schema:
86+
type: object
87+
application/fhir+xml:
88+
schema:
89+
type: object
90+
parameters:
91+
- in: query
92+
name: type
93+
schema:
94+
type: string
95+
description: xml or json
96+
- in: query
97+
name: toType
98+
schema:
99+
type: string
100+
description: xml or json
101+
- in: query
102+
name: version
103+
schema:
104+
type: string
105+
description: source FHIR version (takes precedence over fhirVersion parameter of Content-Type header)
106+
- in: query
107+
name: toVersion
108+
schema:
109+
type: string
110+
description: target FHIR version (takes precedence over fhirVersion parameter of Accept header)
111+
responses:
112+
"200":
113+
description: OK
114+
headers:
115+
Content-Type:
116+
schema:
117+
type: string
118+
"400":
119+
description: Bad Request
120+
content:
121+
text/plain:
122+
schema:
123+
title: convertBadRequest
124+
type: string
125+
example: "Invalid toType parameter! Supported xml or json."
126+
"500":
127+
description: Internal Server Error
128+
content:
129+
text/plain:
130+
schema:
131+
title: convertInternalServerError
132+
type: string
133+
example: "Internal server error."
64134
/ig:
65135
get:
66136
tags:

src/jvmMain/kotlin/Module.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import com.fasterxml.jackson.databind.DeserializationFeature
22
import com.fasterxml.jackson.databind.SerializationFeature
3+
import controller.conversion.conversionModule
34
import controller.debug.debugModule
45
import controller.ig.igModule
56
import controller.terminology.terminologyModule
@@ -98,6 +99,7 @@ fun Application.setup() {
9899
versionModule()
99100
debugModule()
100101
validationModule()
102+
conversionModule()
101103
terminologyModule()
102104
uptimeModule()
103105

src/jvmMain/kotlin/controller/ControllersInjection.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package controller
22

3+
import controller.conversion.ConversionController
4+
import controller.conversion.ConversionControllerImpl
35
import controller.ig.IgController
46
import controller.ig.IgControllerImpl
57
import controller.terminology.TerminologyController
@@ -15,6 +17,7 @@ import org.koin.dsl.module
1517
object ControllersInjection {
1618
val koinBeans = module {
1719
single<ValidationController> { ValidationControllerImpl() }
20+
single<ConversionController> { ConversionControllerImpl() }
1821
single<VersionController> { VersionControllerImpl() }
1922
single<IgController> { IgControllerImpl() }
2023
single<TerminologyController> { TerminologyControllerImpl() }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package controller.conversion
2+
3+
interface ConversionController {
4+
suspend fun convertRequest(content: String, type: String? = "json", version: String? = "5.0", toType: String? = type,
5+
toVersion: String? = version): String
6+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package controller.conversion
2+
3+
import controller.validation.ValidationServiceFactory
4+
import model.CliContext
5+
import org.hl7.fhir.validation.ValidationEngine
6+
import org.koin.core.component.KoinComponent
7+
import org.koin.core.component.inject
8+
import java.nio.file.Files
9+
import java.nio.file.Path
10+
import kotlin.io.path.deleteIfExists
11+
12+
class ConversionControllerImpl : ConversionController, KoinComponent {
13+
14+
private val validationServiceFactory by inject<ValidationServiceFactory>()
15+
16+
override suspend fun convertRequest(content: String, type: String?, version: String?, toType: String?,
17+
toVersion: String?): String {
18+
val fromType = type ?: "json"
19+
val fromVersion = version ?: "5.0"
20+
21+
val cliContext = CliContext()
22+
cliContext.sv = fromVersion
23+
cliContext.targetVer = toVersion ?: fromVersion
24+
25+
var validator: ValidationEngine? = validationServiceFactory.getValidationEngine(cliContext)
26+
27+
var input: Path? = null
28+
var output: Path? = null
29+
try {
30+
input = Files.createTempFile("input", ".$fromType")
31+
Files.writeString(input.toAbsolutePath(), content)
32+
cliContext.addSource(input.toAbsolutePath().toString())
33+
34+
output = Files.createTempFile("convert", ".${toType ?: fromType}")
35+
cliContext.output = output.toAbsolutePath().toString()
36+
37+
validationServiceFactory.getValidationService().convertSources(cliContext, validator)
38+
return Files.readString(output.toAbsolutePath())
39+
} finally {
40+
input?.deleteIfExists()
41+
output?.deleteIfExists()
42+
}
43+
}
44+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package controller.conversion
2+
3+
import constants.CONVERSION_ENDPOINT
4+
5+
import io.ktor.http.*
6+
import io.ktor.server.application.*
7+
import io.ktor.server.request.*
8+
import io.ktor.server.response.*
9+
import io.ktor.server.routing.*
10+
import org.koin.ktor.ext.inject
11+
12+
const val NO_CONTENT_PROVIDED_MESSAGE = "No content for conversion provided in request."
13+
const val INVALID_TYPE_MESSAGE = "Invalid type parameter! Supported xml or json."
14+
const val INVALID_TO_TYPE_MESSAGE = "Invalid toType parameter! Supported xml or json."
15+
16+
fun Route.conversionModule() {
17+
18+
val conversionController by inject<ConversionController>()
19+
20+
post(CONVERSION_ENDPOINT) {
21+
val fhirJson = ContentType("application", "fhir+json")
22+
val fhirXml = ContentType("application", "fhir+xml")
23+
24+
val logger = call.application.environment.log
25+
val content = call.receiveText()
26+
val type = call.request.queryParameters["type"]?.lowercase() ?: when {
27+
call.request.contentType() == ContentType.Application.Json -> "json"
28+
call.request.contentType() == ContentType.Application.Xml -> "xml"
29+
call.request.contentType().withoutParameters() == fhirJson -> "json"
30+
call.request.contentType().withoutParameters() == fhirXml -> "xml"
31+
else -> call.request.contentType().contentSubtype
32+
}
33+
val version = call.request.queryParameters["version"] ?:
34+
call.request.contentType().parameter("fhirVersion") ?: "5.0"
35+
36+
37+
val acceptContentType = if(call.request.accept() != null)
38+
ContentType.parse(call.request.accept().toString()) else null
39+
40+
val toType = call.request.queryParameters["toType"]?.lowercase() ?: when {
41+
acceptContentType == ContentType.Application.Json -> "json"
42+
acceptContentType == ContentType.Application.Xml -> "xml"
43+
acceptContentType?.withoutParameters() == fhirJson -> "json"
44+
acceptContentType?.withoutParameters() == fhirXml -> "xml"
45+
call.request.accept() != null -> acceptContentType?.contentSubtype
46+
else -> type
47+
}
48+
val toVersion = call.request.queryParameters["toVersion"] ?:
49+
acceptContentType?.parameter("fhirVersion") ?: version
50+
51+
logger.info("Received Conversion Request. Convert to $toVersion FHIR version and $toType type. " +
52+
"Memory (free/max): ${java.lang.Runtime.getRuntime().freeMemory()}/" +
53+
"${java.lang.Runtime.getRuntime().maxMemory()}")
54+
55+
when {
56+
content.isEmpty() -> {
57+
call.respond(HttpStatusCode.BadRequest, NO_CONTENT_PROVIDED_MESSAGE)
58+
}
59+
type != "xml" && type != "json" -> {
60+
call.respond(HttpStatusCode.BadRequest, INVALID_TYPE_MESSAGE)
61+
}
62+
toType != "xml" && toType != "json" -> {
63+
call.respond(HttpStatusCode.BadRequest, INVALID_TO_TYPE_MESSAGE)
64+
}
65+
66+
else -> {
67+
try {
68+
val response = conversionController.convertRequest(content, type, version, toType,
69+
toVersion)
70+
val contentType = when {
71+
toType == "xml" -> fhirXml.withParameter("fhirVersion", toVersion)
72+
toType == "json" -> fhirJson.withParameter("fhirVersion", toVersion)
73+
else -> acceptContentType?.withParameter("fhirVersion", toVersion)
74+
}
75+
call.respondText(response, contentType, HttpStatusCode.OK)
76+
} catch (e: Exception) {
77+
logger.error(e.localizedMessage, e)
78+
call.respond(HttpStatusCode.InternalServerError, e.localizedMessage)
79+
}
80+
}
81+
}
82+
}
83+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package controller.validation
22

3+
import org.hl7.fhir.validation.ValidationEngine
4+
import org.hl7.fhir.validation.cli.model.CliContext
35
import org.hl7.fhir.validation.cli.services.ValidationService
46

57
interface ValidationServiceFactory {
68

79
fun getValidationService() : ValidationService
10+
11+
fun getValidationEngine(cliContext: CliContext) : ValidationEngine?
812
}

src/jvmMain/kotlin/controller/validation/ValidationServiceFactoryImpl.kt

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,51 @@
11
package controller.validation
22

3-
import java.util.concurrent.TimeUnit;
4-
5-
import org.hl7.fhir.validation.cli.services.ValidationService
6-
import org.hl7.fhir.validation.cli.services.SessionCache
3+
import org.hl7.fhir.utilities.TimeTracker
4+
import org.hl7.fhir.utilities.VersionUtilities
5+
import org.hl7.fhir.validation.ValidationEngine
6+
import org.hl7.fhir.validation.cli.model.CliContext
77
import org.hl7.fhir.validation.cli.services.PassiveExpiringSessionCache
8+
import org.hl7.fhir.validation.cli.services.SessionCache
9+
import org.hl7.fhir.validation.cli.services.ValidationService
10+
import java.util.concurrent.TimeUnit
811

912
private const val MIN_FREE_MEMORY = 250000000
1013
private const val SESSION_DEFAULT_DURATION: Long = 60
1114

1215
class ValidationServiceFactoryImpl : ValidationServiceFactory {
13-
private var validationService: ValidationService
16+
@Volatile private var validationService: ValidationService = createValidationServiceInstance()
17+
@Volatile private var sessionCache: SessionCache = createSessionCacheInstance()
1418

15-
init {
16-
validationService = createValidationServiceInstance();
19+
private fun createSessionCacheInstance(): SessionCache {
20+
val sessionCacheDuration = System.getenv("SESSION_CACHE_DURATION")?.toLong() ?: SESSION_DEFAULT_DURATION
21+
return PassiveExpiringSessionCache(sessionCacheDuration, TimeUnit.MINUTES).setResetExpirationAfterFetch(true)
1722
}
23+
private fun createValidationServiceInstance() : ValidationService {
24+
sessionCache = createSessionCacheInstance()
25+
return ValidationService(sessionCache)
26+
}
27+
28+
override fun getValidationEngine(cliContext: CliContext): ValidationEngine? {
29+
var definitions = "hl7.fhir.r5.core#current"
30+
if ("dev" != cliContext.sv) {
31+
definitions =
32+
VersionUtilities.packageForVersion(cliContext.sv) + "#" +
33+
VersionUtilities.getCurrentVersion(cliContext.sv)
34+
}
35+
36+
var validationEngine = sessionCache.fetchSessionValidatorEngine(definitions)
37+
if (validationEngine == null) {
38+
validationEngine = getValidationService().initializeValidator(cliContext, definitions, TimeTracker())
39+
sessionCache.cacheSession(definitions, validationEngine)
40+
}
1841

19-
fun createValidationServiceInstance() : ValidationService {
20-
val sessionCacheDuration = System.getenv("SESSION_CACHE_DURATION")?.toLong() ?: SESSION_DEFAULT_DURATION;
21-
val sessionCache: SessionCache = PassiveExpiringSessionCache(sessionCacheDuration, TimeUnit.MINUTES).setResetExpirationAfterFetch(true);
22-
return ValidationService(sessionCache);
42+
return validationEngine
2343
}
2444

2545
override fun getValidationService() : ValidationService {
2646
if (java.lang.Runtime.getRuntime().freeMemory() < MIN_FREE_MEMORY) {
2747
println("Free memory ${java.lang.Runtime.getRuntime().freeMemory()} is less than ${MIN_FREE_MEMORY}. Re-initializing validationService");
28-
validationService = createValidationServiceInstance();
48+
validationService = createValidationServiceInstance()
2949
}
3050
return validationService;
3151
}

0 commit comments

Comments
 (0)