Skip to content

Commit 5490b62

Browse files
committed
Implement /convert endpoint for xml/json conversion (#165)
1 parent 0fe78c5 commit 5490b62

File tree

9 files changed

+230
-6
lines changed

9 files changed

+230
-6
lines changed

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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,81 @@ 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+
requestBody:
74+
required: true
75+
content:
76+
application/json:
77+
schema:
78+
type: object
79+
application/xml:
80+
schema:
81+
type: object
82+
parameters:
83+
- in: query
84+
name: type
85+
schema:
86+
type: string
87+
description: xml or json
88+
- in: query
89+
name: toType
90+
schema:
91+
type: string
92+
description: xml or json
93+
- in: query
94+
name: version
95+
schema:
96+
type: string
97+
description: source FHIR version
98+
- in: query
99+
name: toVersion
100+
schema:
101+
type: string
102+
description: target FHIR version
103+
- in: query
104+
name: sessionId
105+
schema:
106+
type: string
107+
description: sessionId to reuse cached validator
108+
- in: header
109+
name: Session-Id
110+
schema:
111+
type: string
112+
format: uuid
113+
description: sessionId to reuse cached validator
114+
responses:
115+
"200":
116+
description: OK
117+
headers:
118+
Session-Id:
119+
schema:
120+
type: string
121+
format: uuid
122+
description: sessionId to reuse cached validator in consecutive calls
123+
"400":
124+
description: Bad Request
125+
content:
126+
text/plain:
127+
schema:
128+
title: convertBadRequest
129+
type: string
130+
example: "Invalid toType parameter! Supported xml or json."
131+
"500":
132+
description: Internal Server Error
133+
content:
134+
text/plain:
135+
schema:
136+
title: convertInternalServerError
137+
type: string
138+
example: "Internal server error."
64139
/ig:
65140
get:
66141
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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, sessionId: String?): ConversionResponse
6+
}
7+
8+
data class ConversionResponse(val response: String, val sessionId: String)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package controller.conversion
2+
3+
import controller.validation.ValidationServiceFactory
4+
import model.CliContext
5+
import org.hl7.fhir.utilities.TimeTracker
6+
import org.hl7.fhir.utilities.VersionUtilities
7+
import org.hl7.fhir.validation.ValidationEngine
8+
import org.koin.core.component.KoinComponent
9+
import org.koin.core.component.inject
10+
import java.nio.file.Files
11+
import java.nio.file.Path
12+
import kotlin.io.path.deleteIfExists
13+
14+
class ConversionControllerImpl : ConversionController, KoinComponent {
15+
16+
private val validationServiceFactory by inject<ValidationServiceFactory>()
17+
18+
override suspend fun convertRequest(content: String, type: String?, version: String?, toType: String?,
19+
toVersion: String?, sessionId: String?): ConversionResponse {
20+
var fromType = type ?: "json"
21+
var fromVersion = version ?: "5.0"
22+
var session = sessionId ?: "new"
23+
24+
val cliContext = CliContext()
25+
26+
var validator: ValidationEngine? = validationServiceFactory.getCachedValidator(session)
27+
if (validator == null || validator.version != fromVersion) {
28+
val definitions = VersionUtilities.packageForVersion(fromVersion) + "#" +
29+
VersionUtilities.getCurrentVersion(fromVersion)
30+
val timeTracker = TimeTracker()
31+
session = validationServiceFactory.getValidationService()
32+
.initializeValidator(cliContext, definitions, timeTracker, "new")
33+
validator = validationServiceFactory.getCachedValidator(session)
34+
validator?.version = fromVersion
35+
}
36+
37+
var input: Path? = null
38+
var output: Path? = null
39+
try {
40+
input = Files.createTempFile("input", ".$fromType")
41+
Files.writeString(input.toAbsolutePath(), content)
42+
cliContext.addSource(input.toAbsolutePath().toString())
43+
44+
output = Files.createTempFile("convert", ".${toType ?: fromType}")
45+
46+
cliContext.targetVer = toVersion ?: fromVersion
47+
48+
cliContext.output = output.toAbsolutePath().toString()
49+
validationServiceFactory.getValidationService().convertSources(cliContext, validator)
50+
return ConversionResponse(Files.readString(output.toAbsolutePath()), session)
51+
} finally {
52+
input?.deleteIfExists()
53+
output?.deleteIfExists()
54+
}
55+
}
56+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 logger = call.application.environment.log
22+
val content = call.receiveText()
23+
val type = call.request.queryParameters["type"]?.lowercase() ?: when {
24+
call.request.contentType() == ContentType.Application.Xml -> "xml"
25+
call.request.contentType() == ContentType.Application.Json -> "json"
26+
else -> "json"
27+
}
28+
val version = call.request.queryParameters["version"] ?: "5.0"
29+
val toVersion = call.request.queryParameters["toVersion"] ?: version
30+
val toType = call.request.queryParameters["toType"]?.lowercase() ?: type
31+
var sessionId = call.request.queryParameters["sessionId"] ?: call.request.header("Session-Id")
32+
33+
logger.info("Received Conversion Request. Convert to $toVersion FHIR version and $toType type. " +
34+
"Memory (free/max): ${java.lang.Runtime.getRuntime().freeMemory()}/" +
35+
"${java.lang.Runtime.getRuntime().maxMemory()}")
36+
37+
when {
38+
content.isEmpty() -> {
39+
call.respond(HttpStatusCode.BadRequest, NO_CONTENT_PROVIDED_MESSAGE)
40+
}
41+
type != "xml" && type != "json" -> {
42+
call.respond(HttpStatusCode.BadRequest, INVALID_TYPE_MESSAGE)
43+
}
44+
toType != "xml" && toType != "json" -> {
45+
call.respond(HttpStatusCode.BadRequest, INVALID_TO_TYPE_MESSAGE)
46+
}
47+
48+
else -> {
49+
try {
50+
val (response, session) = conversionController.convertRequest(content, type, version, toType,
51+
toVersion, sessionId)
52+
val contentType = if (toType == "xml") ContentType.Application.Xml else ContentType.Application.Json
53+
call.response.headers.append("Session-Id", session)
54+
call.respondText(response, contentType, HttpStatusCode.OK)
55+
} catch (e: Exception) {
56+
logger.error(e.localizedMessage, e)
57+
call.respond(HttpStatusCode.InternalServerError, e.localizedMessage)
58+
}
59+
}
60+
}
61+
}
62+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package controller.validation
22

3+
import org.hl7.fhir.validation.ValidationEngine
34
import org.hl7.fhir.validation.cli.services.ValidationService
45

56
interface ValidationServiceFactory {
67

78
fun getValidationService() : ValidationService
9+
10+
fun getCachedValidator(sessionId: String?) : ValidationEngine?
811
}

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package controller.validation
22

3+
import org.hl7.fhir.validation.ValidationEngine
34
import java.util.concurrent.TimeUnit;
45

56
import org.hl7.fhir.validation.cli.services.ValidationService
@@ -11,21 +12,34 @@ private const val SESSION_DEFAULT_DURATION: Long = 60
1112

1213
class ValidationServiceFactoryImpl : ValidationServiceFactory {
1314
private var validationService: ValidationService
15+
private var sessionCache: SessionCache
1416

1517
init {
16-
validationService = createValidationServiceInstance();
18+
sessionCache = createSessionCacheInstance()
19+
validationService = createValidationServiceInstance()
1720
}
1821

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);
22+
private fun createSessionCacheInstance(): SessionCache {
23+
val sessionCacheDuration = System.getenv("SESSION_CACHE_DURATION")?.toLong() ?: SESSION_DEFAULT_DURATION
24+
return PassiveExpiringSessionCache(sessionCacheDuration, TimeUnit.MINUTES).setResetExpirationAfterFetch(true)
25+
}
26+
private fun createValidationServiceInstance() : ValidationService {
27+
sessionCache = createSessionCacheInstance()
28+
return ValidationService(sessionCache)
29+
}
30+
31+
override fun getCachedValidator(sessionId: String?) : ValidationEngine? {
32+
return if (sessionId != null && sessionCache.sessionExists(sessionId)) {
33+
sessionCache.fetchSessionValidatorEngine(sessionId)
34+
} else {
35+
null
36+
}
2337
}
2438

2539
override fun getValidationService() : ValidationService {
2640
if (java.lang.Runtime.getRuntime().freeMemory() < MIN_FREE_MEMORY) {
2741
println("Free memory ${java.lang.Runtime.getRuntime().freeMemory()} is less than ${MIN_FREE_MEMORY}. Re-initializing validationService");
28-
validationService = createValidationServiceInstance();
42+
validationService = createValidationServiceInstance()
2943
}
3044
return validationService;
3145
}

0 commit comments

Comments
 (0)