Skip to content

Commit

Permalink
Implement /convert endpoint for xml/json conversion (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkorytkowski committed Mar 7, 2024
1 parent 0fe78c5 commit 5490b62
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/commonMain/kotlin/constants/Endpoints.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package constants

const val VALIDATION_ENDPOINT = "validate"
const val CONVERSION_ENDPOINT = "convert"
const val VALIDATOR_VERSION_ENDPOINT = "validator/version"
const val CONTEXT_ENDPOINT = "context"
const val IG_ENDPOINT = "ig"
Expand Down
75 changes: 75 additions & 0 deletions src/commonMain/resources/static-content/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,81 @@ paths:
title: validatorVersionOK
type: string
example: "5.6.39"
/convert:
post:
tags:
- Convert a Resource
description: "Converts a resource."
operationId: ConvertAResource
produces:
- application/json
- application/xml
requestBody:
required: true
content:
application/json:
schema:
type: object
application/xml:
schema:
type: object
parameters:
- in: query
name: type
schema:
type: string
description: xml or json
- in: query
name: toType
schema:
type: string
description: xml or json
- in: query
name: version
schema:
type: string
description: source FHIR version
- in: query
name: toVersion
schema:
type: string
description: target FHIR version
- in: query
name: sessionId
schema:
type: string
description: sessionId to reuse cached validator
- in: header
name: Session-Id
schema:
type: string
format: uuid
description: sessionId to reuse cached validator
responses:
"200":
description: OK
headers:
Session-Id:
schema:
type: string
format: uuid
description: sessionId to reuse cached validator in consecutive calls
"400":
description: Bad Request
content:
text/plain:
schema:
title: convertBadRequest
type: string
example: "Invalid toType parameter! Supported xml or json."
"500":
description: Internal Server Error
content:
text/plain:
schema:
title: convertInternalServerError
type: string
example: "Internal server error."
/ig:
get:
tags:
Expand Down
2 changes: 2 additions & 0 deletions src/jvmMain/kotlin/Module.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.SerializationFeature
import controller.conversion.conversionModule
import controller.debug.debugModule
import controller.ig.igModule
import controller.terminology.terminologyModule
Expand Down Expand Up @@ -98,6 +99,7 @@ fun Application.setup() {
versionModule()
debugModule()
validationModule()
conversionModule()
terminologyModule()
uptimeModule()

Expand Down
3 changes: 3 additions & 0 deletions src/jvmMain/kotlin/controller/ControllersInjection.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package controller

import controller.conversion.ConversionController
import controller.conversion.ConversionControllerImpl
import controller.ig.IgController
import controller.ig.IgControllerImpl
import controller.terminology.TerminologyController
Expand All @@ -15,6 +17,7 @@ import org.koin.dsl.module
object ControllersInjection {
val koinBeans = module {
single<ValidationController> { ValidationControllerImpl() }
single<ConversionController> { ConversionControllerImpl() }
single<VersionController> { VersionControllerImpl() }
single<IgController> { IgControllerImpl() }
single<TerminologyController> { TerminologyControllerImpl() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package controller.conversion

interface ConversionController {
suspend fun convertRequest(content: String, type: String? = "json", version: String? = "5.0", toType: String? = type,
toVersion: String? = version, sessionId: String?): ConversionResponse
}

data class ConversionResponse(val response: String, val sessionId: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package controller.conversion

import controller.validation.ValidationServiceFactory
import model.CliContext
import org.hl7.fhir.utilities.TimeTracker
import org.hl7.fhir.utilities.VersionUtilities
import org.hl7.fhir.validation.ValidationEngine
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.deleteIfExists

class ConversionControllerImpl : ConversionController, KoinComponent {

private val validationServiceFactory by inject<ValidationServiceFactory>()

override suspend fun convertRequest(content: String, type: String?, version: String?, toType: String?,
toVersion: String?, sessionId: String?): ConversionResponse {
var fromType = type ?: "json"
var fromVersion = version ?: "5.0"
var session = sessionId ?: "new"

val cliContext = CliContext()

var validator: ValidationEngine? = validationServiceFactory.getCachedValidator(session)
if (validator == null || validator.version != fromVersion) {
val definitions = VersionUtilities.packageForVersion(fromVersion) + "#" +
VersionUtilities.getCurrentVersion(fromVersion)
val timeTracker = TimeTracker()
session = validationServiceFactory.getValidationService()
.initializeValidator(cliContext, definitions, timeTracker, "new")
validator = validationServiceFactory.getCachedValidator(session)
validator?.version = fromVersion
}

var input: Path? = null
var output: Path? = null
try {
input = Files.createTempFile("input", ".$fromType")
Files.writeString(input.toAbsolutePath(), content)
cliContext.addSource(input.toAbsolutePath().toString())

output = Files.createTempFile("convert", ".${toType ?: fromType}")

cliContext.targetVer = toVersion ?: fromVersion

cliContext.output = output.toAbsolutePath().toString()
validationServiceFactory.getValidationService().convertSources(cliContext, validator)
return ConversionResponse(Files.readString(output.toAbsolutePath()), session)
} finally {
input?.deleteIfExists()
output?.deleteIfExists()
}
}
}
62 changes: 62 additions & 0 deletions src/jvmMain/kotlin/controller/conversion/ConversionModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package controller.conversion

import constants.CONVERSION_ENDPOINT

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject

const val NO_CONTENT_PROVIDED_MESSAGE = "No content for conversion provided in request."
const val INVALID_TYPE_MESSAGE = "Invalid type parameter! Supported xml or json."
const val INVALID_TO_TYPE_MESSAGE = "Invalid toType parameter! Supported xml or json."

fun Route.conversionModule() {

val conversionController by inject<ConversionController>()

post(CONVERSION_ENDPOINT) {
val logger = call.application.environment.log
val content = call.receiveText()
val type = call.request.queryParameters["type"]?.lowercase() ?: when {
call.request.contentType() == ContentType.Application.Xml -> "xml"
call.request.contentType() == ContentType.Application.Json -> "json"
else -> "json"
}
val version = call.request.queryParameters["version"] ?: "5.0"
val toVersion = call.request.queryParameters["toVersion"] ?: version
val toType = call.request.queryParameters["toType"]?.lowercase() ?: type
var sessionId = call.request.queryParameters["sessionId"] ?: call.request.header("Session-Id")

logger.info("Received Conversion Request. Convert to $toVersion FHIR version and $toType type. " +
"Memory (free/max): ${java.lang.Runtime.getRuntime().freeMemory()}/" +
"${java.lang.Runtime.getRuntime().maxMemory()}")

when {
content.isEmpty() -> {
call.respond(HttpStatusCode.BadRequest, NO_CONTENT_PROVIDED_MESSAGE)
}
type != "xml" && type != "json" -> {
call.respond(HttpStatusCode.BadRequest, INVALID_TYPE_MESSAGE)
}
toType != "xml" && toType != "json" -> {
call.respond(HttpStatusCode.BadRequest, INVALID_TO_TYPE_MESSAGE)
}

else -> {
try {
val (response, session) = conversionController.convertRequest(content, type, version, toType,
toVersion, sessionId)
val contentType = if (toType == "xml") ContentType.Application.Xml else ContentType.Application.Json
call.response.headers.append("Session-Id", session)
call.respondText(response, contentType, HttpStatusCode.OK)
} catch (e: Exception) {
logger.error(e.localizedMessage, e)
call.respond(HttpStatusCode.InternalServerError, e.localizedMessage)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package controller.validation

import org.hl7.fhir.validation.ValidationEngine
import org.hl7.fhir.validation.cli.services.ValidationService

interface ValidationServiceFactory {

fun getValidationService() : ValidationService

fun getCachedValidator(sessionId: String?) : ValidationEngine?
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package controller.validation

import org.hl7.fhir.validation.ValidationEngine
import java.util.concurrent.TimeUnit;

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

class ValidationServiceFactoryImpl : ValidationServiceFactory {
private var validationService: ValidationService
private var sessionCache: SessionCache

init {
validationService = createValidationServiceInstance();
sessionCache = createSessionCacheInstance()
validationService = createValidationServiceInstance()
}

fun createValidationServiceInstance() : ValidationService {
val sessionCacheDuration = System.getenv("SESSION_CACHE_DURATION")?.toLong() ?: SESSION_DEFAULT_DURATION;
val sessionCache: SessionCache = PassiveExpiringSessionCache(sessionCacheDuration, TimeUnit.MINUTES).setResetExpirationAfterFetch(true);
return ValidationService(sessionCache);
private fun createSessionCacheInstance(): SessionCache {
val sessionCacheDuration = System.getenv("SESSION_CACHE_DURATION")?.toLong() ?: SESSION_DEFAULT_DURATION
return PassiveExpiringSessionCache(sessionCacheDuration, TimeUnit.MINUTES).setResetExpirationAfterFetch(true)
}
private fun createValidationServiceInstance() : ValidationService {
sessionCache = createSessionCacheInstance()
return ValidationService(sessionCache)
}

override fun getCachedValidator(sessionId: String?) : ValidationEngine? {
return if (sessionId != null && sessionCache.sessionExists(sessionId)) {
sessionCache.fetchSessionValidatorEngine(sessionId)
} else {
null
}
}

override fun getValidationService() : ValidationService {
if (java.lang.Runtime.getRuntime().freeMemory() < MIN_FREE_MEMORY) {
println("Free memory ${java.lang.Runtime.getRuntime().freeMemory()} is less than ${MIN_FREE_MEMORY}. Re-initializing validationService");
validationService = createValidationServiceInstance();
validationService = createValidationServiceInstance()
}
return validationService;
}
Expand Down

0 comments on commit 5490b62

Please sign in to comment.