From 297208c6217812195731a9a3c3e5328b1ca9bb78 Mon Sep 17 00:00:00 2001 From: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:11:09 +0300 Subject: [PATCH 1/4] Add spotless check task and run the check for the existing files. (#283) Signed-off-by: Lentumunai-Mark Co-authored-by: Peter Lubell-Doughtie --- sm-gen/build.gradle.kts | 75 +- sm-gen/gradlew | 0 sm-gen/settings.gradle.kts | 2 - .../FhirPathEngineHostServices.kt | 150 ++-- .../Main.kt | 502 +++++------ .../TransformSupportServices.kt | 87 +- .../Utils.kt | 817 +++++++++--------- .../FhirPathEngineHostServicesTest.kt | 140 +-- 8 files changed, 919 insertions(+), 854 deletions(-) mode change 100644 => 100755 sm-gen/gradlew diff --git a/sm-gen/build.gradle.kts b/sm-gen/build.gradle.kts index dae53268..dc3a3276 100644 --- a/sm-gen/build.gradle.kts +++ b/sm-gen/build.gradle.kts @@ -1,55 +1,76 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.gradle.jvm.tasks.Jar +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.6.21" + kotlin("jvm") version "1.8.0" + id("com.diffplug.spotless") version "6.25.0" } group = "org.example" + version = "1.0-SNAPSHOT" repositories { - mavenCentral() + mavenCentral() + gradlePluginPortal() } dependencies { - testImplementation(kotlin("test")) - implementation("com.github.ajalt.clikt:clikt:3.4.0") - implementation("org.apache.poi:poi:3.17") - implementation("org.apache.poi:poi-ooxml:3.17") - implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.4.0") - implementation("ca.uhn.hapi.fhir:hapi-fhir-validation:5.4.0") - implementation(kotlin("stdlib-jdk8")) + testImplementation(kotlin("test")) + implementation("com.github.ajalt.clikt:clikt:3.4.0") + implementation("org.apache.poi:poi:3.17") + implementation("org.apache.poi:poi-ooxml:3.17") + implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.4.0") + implementation("ca.uhn.hapi.fhir:hapi-fhir-validation:5.4.0") + implementation(kotlin("stdlib-jdk8")) } -tasks.test { - useJUnitPlatform() -} +tasks.test { useJUnitPlatform() } -tasks.withType { - kotlinOptions.jvmTarget = "1.8" -} +tasks.withType { kotlinOptions.jvmTarget = "1.8" } -val fatJar = task("fatJar", type = Jar::class) { +val fatJar = + task("fatJar", type = Jar::class) { baseName = "${project.name}-fat" // manifest Main-Class attribute is optional. // (Used only to provide default main class for executable jar) manifest { - attributes["Main-Class"] = "example.HelloWorldKt" // fully qualified class name of default main class + attributes["Main-Class"] = + "example.HelloWorldKt" // fully qualified class name of default main class } from(configurations.compileClasspath.get().map({ if (it.isDirectory) it else zipTree(it) })) with(tasks["jar"] as CopySpec) -} + } -tasks { - "build" { - dependsOn(fatJar) - } -} +tasks { "build" { dependsOn(fatJar) } } kotlin { - jvmToolchain { - (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(11)) - } + jvmToolchain { (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(11)) } } +spotless { + kotlin { + target("**/*.kt") + ktlint("0.49.0") + ktfmt().googleStyle() + } + + kotlinGradle { + target("*.gradle.kts") + ktlint("0.49.0") + ktfmt().googleStyle() + } + + format("xml") { + target("**/*.xml") + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } + + format("json") { + target("**/*.json") + indentWithSpaces(2) + trimTrailingWhitespace() + } +} diff --git a/sm-gen/gradlew b/sm-gen/gradlew old mode 100644 new mode 100755 diff --git a/sm-gen/settings.gradle.kts b/sm-gen/settings.gradle.kts index 91efe3f5..9ff2e389 100644 --- a/sm-gen/settings.gradle.kts +++ b/sm-gen/settings.gradle.kts @@ -1,3 +1 @@ - rootProject.name = "structure-map-tool" - diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt index 5b7979c2..fd4f5c15 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt @@ -1,97 +1,99 @@ package org.smartregister.fhir.structuremaptool -import org.hl7.fhir.r4.model.* +import org.hl7.fhir.r4.model.Base +import org.hl7.fhir.r4.model.TypeDetails +import org.hl7.fhir.r4.model.ValueSet import org.hl7.fhir.r4.utils.FHIRPathEngine import org.slf4j.LoggerFactory /* -* Resolves constants defined in the fhir path expressions beyond those defined in the specification -*/ -internal object FHIRPathEngineHostServices : FHIRPathEngine.IEvaluationContext { + * Resolves constants defined in the fhir path expressions beyond those defined in the specification + */ +internal object FhirPathEngineHostServices : FHIRPathEngine.IEvaluationContext { - private val logger = LoggerFactory.getLogger(FHIRPathEngineHostServices::class.java) + private val logger = LoggerFactory.getLogger(FhirPathEngineHostServices::class.java) - // Cache to store resolved constants - private val constantCache = mutableMapOf() + // Cache to store resolved constants + private val constantCache = mutableMapOf() - // Cache for function details - private val functionCache = mutableMapOf() + // Cache for function details + private val functionCache = + mutableMapOf() - // Cache for resolved references - private val referenceCache = mutableMapOf() + // Cache for resolved references + private val referenceCache = mutableMapOf() - // Cache for value sets - private val valueSetCache = mutableMapOf() + // Cache for value sets + private val valueSetCache = mutableMapOf() - override fun resolveConstant(appContext: Any?, name: String?, beforeContext: Boolean): Base? { - if (name == null) return null + override fun resolveConstant(appContext: Any?, name: String?, beforeContext: Boolean): Base? { + if (name == null) return null - return constantCache.getOrPut(name) { - (appContext as? Map<*, *>)?.get(name) as? Base - } - } - - override fun resolveConstantType(appContext: Any?, name: String?): TypeDetails { - // Improved logging with null check - logger.info("Resolving constant type for: ${name ?: "null"}") - - if (name.isNullOrEmpty()) { - logger.warn("Cannot resolve constant type for a null or empty string.") - throw IllegalArgumentException("Constant name cannot be null or empty.") - } - - // Placeholder for actual implementation - throw UnsupportedOperationException("resolveConstantType is not yet implemented.") - } + return constantCache.getOrPut(name) { (appContext as? Map<*, *>)?.get(name) as? Base } + } + override fun resolveConstantType(appContext: Any?, name: String?): TypeDetails { + // Improved logging with null check + logger.info("Resolving constant type for: ${name ?: "null"}") - override fun log(argument: String?, focus: MutableList?): Boolean { - logger.info("Logging argument: $argument with focus: $focus") - return true + if (name.isNullOrEmpty()) { + logger.warn("Cannot resolve constant type for a null or empty string.") + throw IllegalArgumentException("Constant name cannot be null or empty.") } - override fun resolveFunction(functionName: String?): FHIRPathEngine.IEvaluationContext.FunctionDetails { - logger.info("Resolving function: ${functionName ?: "Unknown"}") - return functionCache.getOrPut(functionName ?: "") { - throw UnsupportedOperationException("Function $functionName is not yet implemented.") - } + // Placeholder for actual implementation + throw UnsupportedOperationException("resolveConstantType is not yet implemented.") + } + + override fun log(argument: String?, focus: MutableList?): Boolean { + logger.info("Logging argument: $argument with focus: $focus") + return true + } + + override fun resolveFunction( + functionName: String? + ): FHIRPathEngine.IEvaluationContext.FunctionDetails { + logger.info("Resolving function: ${functionName ?: "Unknown"}") + return functionCache.getOrPut(functionName ?: "") { + throw UnsupportedOperationException("Function $functionName is not yet implemented.") } - - override fun checkFunction( - appContext: Any?, - functionName: String?, - parameters: MutableList? - ): TypeDetails { - logger.info("Checking function: $functionName with parameters: $parameters") - throw UnsupportedOperationException("checkFunction is not yet implemented.") + } + + override fun checkFunction( + appContext: Any?, + functionName: String?, + parameters: MutableList?, + ): TypeDetails { + logger.info("Checking function: $functionName with parameters: $parameters") + throw UnsupportedOperationException("checkFunction is not yet implemented.") + } + + override fun executeFunction( + appContext: Any?, + focus: MutableList?, + functionName: String?, + parameters: MutableList>?, + ): MutableList { + logger.info("Executing function: $functionName with parameters: $parameters") + throw UnsupportedOperationException("executeFunction is not yet implemented.") + } + + override fun resolveReference(appContext: Any?, url: String?): Base { + logger.info("Resolving reference for URL: $url") + return referenceCache.getOrPut(url ?: "") { + throw UnsupportedOperationException("resolveReference is not yet implemented.") } + } - override fun executeFunction( - appContext: Any?, - focus: MutableList?, - functionName: String?, - parameters: MutableList>? - ): MutableList { - logger.info("Executing function: $functionName with parameters: $parameters") - throw UnsupportedOperationException("executeFunction is not yet implemented.") - } - - override fun resolveReference(appContext: Any?, url: String?): Base { - logger.info("Resolving reference for URL: $url") - return referenceCache.getOrPut(url ?: "") { - throw UnsupportedOperationException("resolveReference is not yet implemented.") - } - } - - override fun conformsToProfile(appContext: Any?, item: Base?, url: String?): Boolean { - logger.info("Checking if item conforms to profile: $url") - throw UnsupportedOperationException("conformsToProfile is not yet implemented.") - } + override fun conformsToProfile(appContext: Any?, item: Base?, url: String?): Boolean { + logger.info("Checking if item conforms to profile: $url") + throw UnsupportedOperationException("conformsToProfile is not yet implemented.") + } - override fun resolveValueSet(appContext: Any?, url: String?): ValueSet { - logger.info("Resolving ValueSet for URL: $url") - return valueSetCache.getOrPut(url ?: "") { - throw UnsupportedOperationException("resolveValueSet is not yet implemented.") - } + override fun resolveValueSet(appContext: Any?, url: String?): ValueSet { + logger.info("Resolving ValueSet for URL: $url") + return valueSetCache.getOrPut(url ?: "") { + throw UnsupportedOperationException("resolveValueSet is not yet implemented.") } + } } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt index 272e90b2..00e7f137 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt @@ -6,25 +6,25 @@ import ca.uhn.fhir.parser.IParser import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.prompt +import java.io.File +import java.io.FileInputStream +import java.nio.charset.Charset +import java.util.* import org.apache.commons.io.FileUtils import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.Row import org.apache.poi.ss.usermodel.WorkbookFactory import org.hl7.fhir.r4.context.SimpleWorkerContext -import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion -import java.io.File -import java.io.FileInputStream -import java.nio.charset.Charset -import java.util.* fun main(args: Array) { - Application().main(args) + Application().main(args) } /*fun main(args: Array) { @@ -36,7 +36,6 @@ fun main(args: Array) { }*/ - /* REMAINING TASKS @@ -47,292 +46,297 @@ REMAINING TASKS */ class Application : CliktCommand() { - val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") - val questionnairefile : String by option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") - - - override fun run() { - // Create a map of Resource -> questionnaire name or path -> value - // For each resource loop through creating or adding the correct instructions - - lateinit var questionnaireResponse: QuestionnaireResponse - val contextR4 = FhirContext.forR4() - val fhirJsonParser = contextR4.newJsonParser() - val questionnaire : Questionnaire = fhirJsonParser.parseResource(Questionnaire::class.java, FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset())) - val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) - if (questionnaireResponseFile.exists()) { - questionnaireResponse = fhirJsonParser.parseResource(QuestionnaireResponse::class.java, questionnaireResponseFile.readText(Charset.defaultCharset())) - } else { - println("File not found: questionnaire-response.json") - } - - // reads the xls - val xlsFile = FileInputStream(xlsfile) - val xlWb = WorkbookFactory.create(xlsFile) - + val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") + val questionnairefile: String by + option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") + + override fun run() { + // Create a map of Resource -> questionnaire name or path -> value + // For each resource loop through creating or adding the correct instructions + + lateinit var questionnaireResponse: QuestionnaireResponse + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire: Questionnaire = + fhirJsonParser.parseResource( + Questionnaire::class.java, + FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) + ) + val questionnaireResponseFile = + File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) + if (questionnaireResponseFile.exists()) { + questionnaireResponse = + fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, + questionnaireResponseFile.readText(Charset.defaultCharset()) + ) + } else { + println("File not found: questionnaire-response.json") + } - // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. RiskFlag in the previous XLSX was not valid - // TODO: Check that all the path's and other entries in the excel sheet are valid - // TODO: Add instructions for adding embedded classes like `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices + // reads the xls + val xlsFile = FileInputStream(xlsfile) + val xlWb = WorkbookFactory.create(xlsFile) - /* + // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. + // RiskFlag in the previous XLSX was not valid + // TODO: Check that all the path's and other entries in the excel sheet are valid + // TODO: Add instructions for adding embedded classes like + // `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices - READ THE SETTINGS SHEET + /* - */ - val settingsWorkbook = xlWb.getSheet("Settings") - var questionnaireId : String? = null + READ THE SETTINGS SHEET - for (i in 0..settingsWorkbook.lastRowNum) { - val cell = settingsWorkbook.getRow(i).getCell(0) + */ + val settingsWorkbook = xlWb.getSheet("Settings") + var questionnaireId: String? = null - if (cell.stringCellValue == "questionnaire-id") { - questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue - } - } + for (i in 0..settingsWorkbook.lastRowNum) { + val cell = settingsWorkbook.getRow(i).getCell(0) - /* + if (cell.stringCellValue == "questionnaire-id") { + questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue + } + } - END OF READ SETTINGS SHEET + /* - */ + END OF READ SETTINGS SHEET - /* - TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first - We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! - */ - val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } - if(questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()){ + */ - val sb = StringBuilder() - val structureMapHeader = """ + /* + TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first + We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! + */ + val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } + if (questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()) { + val sb = StringBuilder() + val structureMapHeader = + """ map "http://hl7.org/fhir/StructureMap/$questionnaireId" = '${questionnaireId.clean()}' uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireReponse" as source uses "http://hl7.org/fhir/StructureDefinition/Bundle" as target - """.trimIndent() + """ + .trimIndent() - val structureMapBody = """ + val structureMapBody = + """ group ${questionnaireId.clean()}(source src : QuestionnaireResponse, target bundle: Bundle) { src -> bundle.id = uuid() "rule_c"; src -> bundle.type = 'collection' "rule_b"; - src -> bundle.entry as entry then """.trimIndent() - - /* - - Create a mapping of COLUMN_NAMES to COLUMN indexes - - */ - //val mapColumns - - - val lineNos = 1 - var firstResource = true - val extractionResources = hashMapOf() - val resourceConversionInstructions = hashMapOf>() - - // Group the rules according to the resource - val fieldMappingsSheet = xlWb.getSheet("Field Mappings") - fieldMappingsSheet.forEachIndexed { index, row -> - if (index == 0) return@forEachIndexed - - if (row.isEmpty()) { - return@forEachIndexed - } - - - val instruction = row.getInstruction() - val xlsId = instruction.responseFieldId - val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) - if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { - resourceConversionInstructions.computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) - .add(instruction) - } - } - //val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as Resource - - - // Perform the extraction for the row - /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) - - extractionResources[resourceName + resourceIndex] = resource*/ - - sb.append(structureMapHeader) - sb.appendNewLine().appendNewLine().appendNewLine() - sb.append(structureMapBody) - - // Fix the questions path - val questionsPath = getQuestionsPath(questionnaire) - - // TODO: Generate the links to the group names here - var index = 0 - var len = resourceConversionInstructions.size - var resourceName = "" - resourceConversionInstructions.forEach { entry -> - resourceName = entry.key.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - if (index++ != 0) sb.append(",") - if(resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") - } - sb.append(""" "rule_a";""".trimMargin()) - sb.appendNewLine() - sb.append("}") - - // Add the embedded instructions - val groupNames = mutableListOf() - - sb.appendNewLine().appendNewLine().appendNewLine() - - resourceConversionInstructions.forEach { - Group(it, sb, questionsPath) - .generateGroup(questionnaireResponse) - } - - val structureMapString = sb.toString() - try { - val simpleWorkerContext = SimpleWorkerContext().apply { - setExpansionProfile(Parameters()) - isCanRunWithoutTerminology = true - } - val transformSupportServices = TransformSupportServices(simpleWorkerContext) - val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) - val structureMap = scu.parse(structureMapString, questionnaireId.clean()) - // DataFormatException | FHIRLexerException - - try{ - val bundle = Bundle() - scu.transform(contextR4, questionnaireResponse, structureMap, bundle) - val jsonParser = FhirContext.forR4().newJsonParser() - - println(jsonParser.encodeResourceToString(bundle)) - } catch (e:Exception){ - e.printStackTrace() - } - - } catch (ex: Exception) { - println("The generated StructureMap has a formatting error") - ex.printStackTrace() - } - - var finalStructureMap = sb.toString() - finalStructureMap = finalStructureMap.addIdentation() - println(finalStructureMap) - writeStructureMapOutput(sb.toString().addIdentation()) + src -> bundle.entry as entry then + """ + .trimIndent() + + val lineNos = 1 + var firstResource = true + val extractionResources = hashMapOf() + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWb.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + + if (row.isEmpty()) { + return@forEachIndexed } - } - - fun Row.getInstruction() : Instruction { - return Instruction().apply { - responseFieldId = getCell(0) ?.stringCellValue - constantValue = getCellAsString(1) - resource = getCell(2).stringCellValue - resourceIndex = getCell(3) ?.numericCellValue?.toInt() ?: 0 - fieldPath = getCell(4) ?.stringCellValue ?: "" - fullFieldPath = fieldPath - field = getCell(5) ?.stringCellValue - conversion = getCell(6) ?.stringCellValue - fhirPathStructureMapFunctions = getCell(7) ?.stringCellValue + val instruction = row.getInstruction() + val xlsId = instruction.responseFieldId + val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) + if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { + resourceConversionInstructions + .computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) + .add(instruction) } - } - - fun Row.getCellAsString(cellnum: Int) : String? { - val cell = getCell(cellnum) ?: return null - return when (cell.cellTypeEnum) { - CellType.STRING -> cell.stringCellValue - CellType.BLANK -> null - CellType.BOOLEAN -> cell.booleanCellValue.toString() - CellType.NUMERIC -> cell.numericCellValue.toString() - else -> null + } + // val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as + // Resource + + // Perform the extraction for the row + /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) + + extractionResources[resourceName + resourceIndex] = resource*/ + + sb.append(structureMapHeader) + sb.appendNewLine().appendNewLine().appendNewLine() + sb.append(structureMapBody) + + // Fix the questions path + val questionsPath = getQuestionsPath(questionnaire) + + // TODO: Generate the links to the group names here + var index = 0 + var len = resourceConversionInstructions.size + var resourceName = "" + resourceConversionInstructions.forEach { entry -> + resourceName = + entry.key.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + if (index++ != 0) sb.append(",") + if (resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") + } + sb.append(""" "rule_a";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + + // Add the embedded instructions + val groupNames = mutableListOf() + + sb.appendNewLine().appendNewLine().appendNewLine() + + resourceConversionInstructions.forEach { + Group(it, sb, questionsPath).generateGroup(questionnaireResponse) + } + + val structureMapString = sb.toString() + try { + val simpleWorkerContext = + SimpleWorkerContext().apply { + setExpansionProfile(Parameters()) + isCanRunWithoutTerminology = true + } + val transformSupportServices = TransformSupportServices(simpleWorkerContext) + val scu = + org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) + val structureMap = scu.parse(structureMapString, questionnaireId.clean()) + // DataFormatException | FHIRLexerException + + try { + val bundle = Bundle() + scu.transform(contextR4, questionnaireResponse, structureMap, bundle) + val jsonParser = FhirContext.forR4().newJsonParser() + + println(jsonParser.encodeResourceToString(bundle)) + } catch (e: Exception) { + e.printStackTrace() } + } catch (ex: Exception) { + println("The generated StructureMap has a formatting error") + ex.printStackTrace() + } + + var finalStructureMap = sb.toString() + finalStructureMap = finalStructureMap.addIndentation() + println(finalStructureMap) + writeStructureMapOutput(sb.toString().addIndentation()) } - - fun Row.isEmpty() : Boolean { - return getCell(0) == null && getCell(1) == null && getCell(2) == null + } + + fun Row.getInstruction(): Instruction { + return Instruction().apply { + responseFieldId = getCell(0)?.stringCellValue + constantValue = getCellAsString(1) + resource = getCell(2).stringCellValue + resourceIndex = getCell(3)?.numericCellValue?.toInt() ?: 0 + fieldPath = getCell(4)?.stringCellValue ?: "" + fullFieldPath = fieldPath + field = getCell(5)?.stringCellValue + conversion = getCell(6)?.stringCellValue + fhirPathStructureMapFunctions = getCell(7)?.stringCellValue } - - fun String.clean() : String { - return this.replace("-", "") - .replace("_", "") - .replace(" ", "") + } + + fun Row.getCellAsString(cellnum: Int): String? { + val cell = getCell(cellnum) ?: return null + return when (cell.cellTypeEnum) { + CellType.STRING -> cell.stringCellValue + CellType.BLANK -> null + CellType.BOOLEAN -> cell.booleanCellValue.toString() + CellType.NUMERIC -> cell.numericCellValue.toString() + else -> null } + } + + fun Row.isEmpty(): Boolean { + return getCell(0) == null && getCell(1) == null && getCell(2) == null + } + fun String.clean(): String { + return this.replace("-", "").replace("_", "").replace(" ", "") + } } class Instruction { - var responseFieldId : String? = null - var constantValue: String? = null - var resource: String = "" - var resourceIndex: Int = 0 - var fieldPath: String = "" - var field: String? = null - var conversion : String? = null - var fhirPathStructureMapFunctions: String? = null + var responseFieldId: String? = null + var constantValue: String? = null + var resource: String = "" + var resourceIndex: Int = 0 + var fieldPath: String = "" + var field: String? = null + var conversion: String? = null + var fhirPathStructureMapFunctions: String? = null + // TODO: Clean the following properties + var fullFieldPath = "" - // TODO: Clean the following properties - var fullFieldPath = "" - fun fullPropertyPath() : String = "$resource.$fullFieldPath" + fun fullPropertyPath(): String = "$resource.$fullFieldPath" - fun searchKey() = resource + resourceIndex + fun searchKey() = resource + resourceIndex } fun Instruction.copyFrom(instruction: Instruction) { - constantValue = instruction.constantValue - resource = instruction.resource - resourceIndex = instruction.resourceIndex - fieldPath = instruction.fieldPath - fullFieldPath = instruction.fullFieldPath - field = instruction.field - conversion = instruction.conversion - fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions + constantValue = instruction.constantValue + resource = instruction.resource + resourceIndex = instruction.resourceIndex + fieldPath = instruction.fieldPath + fullFieldPath = instruction.fullFieldPath + field = instruction.field + conversion = instruction.conversion + fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions } - -fun String.addIdentation() : String { - var currLevel = 0 - - val lines = split("\n") - - val sb = StringBuilder() - lines.forEach { line -> - if (line.endsWith("{")) { - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - currLevel++ - } else if (line.startsWith("}")) { - currLevel-- - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - } else { - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - } +fun String.addIndentation(): String { + var currLevel = 0 + + val lines = split("\n") + + val sb = StringBuilder() + lines.forEach { line -> + if (line.endsWith("{")) { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + currLevel++ + } else if (line.startsWith("}")) { + currLevel-- + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + } else { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() } + } - return sb.toString() + return sb.toString() } -fun String.addIdentation(times: Int) : String { - var processedString = "" - for (k in 1..times) { - processedString += "\t" - } +fun String.addIndentation(times: Int): String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } - processedString += this - return processedString + processedString += this + return processedString } -fun writeStructureMapOutput( structureMap: String){ - File("generated-structure-map.txt").writeText(structureMap.addIdentation()) - val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) - val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) - contextR5.setExpansionProfile(Parameters()) - contextR5.isCanRunWithoutTerminology = true - val transformSupportServices = TransformSupportServices(contextR5) - val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) - val map = scu.parse(structureMap, "LocationRegistration") - val iParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) - val mapString = iParser.encodeResourceToString(map) - File("generated-json-map.json").writeText(mapString) +fun writeStructureMapOutput(structureMap: String) { + File("generated-structure-map.txt").writeText(structureMap.addIndentation()) + val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) + val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) + contextR5.setExpansionProfile(Parameters()) + contextR5.isCanRunWithoutTerminology = true + val transformSupportServices = TransformSupportServices(contextR5) + val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) + val map = scu.parse(structureMap, "LocationRegistration") + val iParser: IParser = + FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) + val mapString = iParser.encodeResourceToString(map) + File("generated-json-map.json").writeText(mapString) } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt index 70a007f4..e638a22b 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt @@ -1,9 +1,7 @@ package org.smartregister.fhir.structuremaptool -import org.hl7.fhir.r4.context.SimpleWorkerContext -import org.hl7.fhir.r4.utils.StructureMapUtilities.ITransformerServices - import org.hl7.fhir.exceptions.FHIRException +import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.Coding @@ -18,55 +16,56 @@ import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.RiskAssessment.RiskAssessmentPredictionComponent import org.hl7.fhir.r4.model.Timing import org.hl7.fhir.r4.terminologies.ConceptMapEngine +import org.hl7.fhir.r4.utils.StructureMapUtilities.ITransformerServices class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorkerContext) : - ITransformerServices { + ITransformerServices { - val outputs: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() - override fun log(message: String) { - System.out.println(message) - } + override fun log(message: String) { + println(message) + } - @Throws(FHIRException::class) - override fun createType(appInfo: Any, name: String): Base { - return when (name) { - "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() - "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() - "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() - "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() - "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() - "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() - "Encounter_Participant" -> Encounter.EncounterParticipantComponent() - "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() - "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() - "Patient_Link" -> Patient.PatientLinkComponent() - "Timing_Repeat" -> Timing.TimingRepeatComponent() - "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() - "Group_Characteristic" -> Group.GroupCharacteristicComponent() - "Observation_Component" -> Observation.ObservationComponentComponent() - else -> ResourceFactory.createResourceOrType(name) - } + @Throws(FHIRException::class) + override fun createType(appInfo: Any, name: String): Base { + return when (name) { + "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() + "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() + "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() + "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() + "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() + "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() + "Encounter_Participant" -> Encounter.EncounterParticipantComponent() + "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() + "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() + "Patient_Link" -> Patient.PatientLinkComponent() + "Timing_Repeat" -> Timing.TimingRepeatComponent() + "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() + "Group_Characteristic" -> Group.GroupCharacteristicComponent() + "Observation_Component" -> Observation.ObservationComponentComponent() + else -> ResourceFactory.createResourceOrType(name) } + } - override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { - if (atRootofTransform) outputs.add(res) - return res - } + override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { + if (atRootofTransform) outputs.add(res) + return res + } - @Throws(FHIRException::class) - override fun translate(appInfo: Any, source: Coding, conceptMapUrl: String): Coding { - val cme = ConceptMapEngine(simpleWorkerContext) - return cme.translate(source, conceptMapUrl) - } + @Throws(FHIRException::class) + override fun translate(appInfo: Any, source: Coding, conceptMapUrl: String): Coding { + val cme = ConceptMapEngine(simpleWorkerContext) + return cme.translate(source, conceptMapUrl) + } - @Throws(FHIRException::class) - override fun resolveReference(appContext: Any, url: String): Base { - throw FHIRException("resolveReference is not supported yet") - } + @Throws(FHIRException::class) + override fun resolveReference(appContext: Any, url: String): Base { + throw FHIRException("resolveReference is not supported yet") + } - @Throws(FHIRException::class) - override fun performSearch(appContext: Any, url: String): List { - throw FHIRException("performSearch is not supported yet") - } + @Throws(FHIRException::class) + override fun performSearch(appContext: Any, url: String): List { + throw FHIRException("performSearch is not supported yet") + } } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt index cdda9b4f..843944d1 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt @@ -2,6 +2,8 @@ package org.smartregister.fhir.structuremaptool import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType import org.apache.poi.ss.usermodel.Row import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Enumeration @@ -11,511 +13,534 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.Type import org.hl7.fhir.r4.utils.FHIRPathEngine -import java.lang.reflect.Field -import java.lang.reflect.ParameterizedType // Get the hl7 resources val contextR4 = FhirContext.forR4() val fhirResources = contextR4.resourceTypes + fun getQuestionsPath(questionnaire: Questionnaire): HashMap { - val questionsMap = hashMapOf() + val questionsMap = hashMapOf() - questionnaire.item.forEach { itemComponent -> - getQuestionNames("", itemComponent, questionsMap) - } - return questionsMap + questionnaire.item.forEach { itemComponent -> getQuestionNames("", itemComponent, questionsMap) } + return questionsMap } -fun getQuestionNames(parentName: String, item: QuestionnaireItemComponent, questionsMap: HashMap) { - val currParentName = if (parentName.isEmpty()) "" else parentName - questionsMap.put(item.linkId, currParentName) - - item.item.forEach { itemComponent -> - getQuestionNames(currParentName + ".where(linkId = '${item.linkId}').item", itemComponent, questionsMap) - } +fun getQuestionNames( + parentName: String, + item: QuestionnaireItemComponent, + questionsMap: HashMap +) { + val currParentName = if (parentName.isEmpty()) "" else parentName + questionsMap.put(item.linkId, currParentName) + + item.item.forEach { itemComponent -> + getQuestionNames( + currParentName + ".where(linkId = '${item.linkId}').item", + itemComponent, + questionsMap + ) + } } - class Group( - entry: Map.Entry>, - val stringBuilder: StringBuilder, - val questionsPath: HashMap + entry: Map.Entry>, + val stringBuilder: StringBuilder, + val questionsPath: HashMap, ) { - var lineCounter = 0 - var groupName = entry.key - val instructions = entry.value - - private fun generateReference(resourceName: String, resourceIndex: String): String { - // Generate the reference based on the resourceName and resourceIndex - val sb = StringBuilder() - sb.append("create('Reference') as reference then {") - sb.appendNewLine() - sb.append("src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))") - sb.append(""" "rule_d";""".trimMargin()) - sb.appendNewLine() - sb.append("}") - return sb.toString() + var lineCounter = 0 + var groupName = entry.key + val instructions = entry.value + + private fun generateReference(resourceName: String, resourceIndex: String): String { + // Generate the reference based on the resourceName and resourceIndex + val sb = StringBuilder() + sb.append("create('Reference') as reference then {") + sb.appendNewLine() + sb.append( + "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))" + ) + sb.append(""" "rule_d";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + return sb.toString() + } + + fun generateGroup(questionnaireResponse: QuestionnaireResponse) { + if (fhirResources.contains(groupName.dropLast(1))) { + val resourceName = instructions[0].resource + + // add target of reference to function if reference is not null + val structureMapFunctionHead = + "group Extract$groupName(source src : QuestionniareResponse, target bundle: Bundle) {" + stringBuilder.appendNewLine() + stringBuilder.append(structureMapFunctionHead).appendNewLine() + stringBuilder + .append( + "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {" + ) + .appendNewLine() + + val mainNest = Nest() + mainNest.fullPath = "" + mainNest.name = "" + mainNest.resourceName = resourceName + + instructions.forEachIndexed { index, instruction -> mainNest.add(instruction) } + + mainNest.buildStructureMap(0, questionnaireResponse) + + stringBuilder.append("} ") + addRuleNo() + stringBuilder.appendNewLine() + stringBuilder.append("}") + stringBuilder.appendNewLine() + stringBuilder.appendNewLine() + } else { + println("$groupName is not a valid hl7 resource name") } - - fun generateGroup(questionnaireResponse: QuestionnaireResponse) { - if(fhirResources.contains(groupName.dropLast(1))){ - val resourceName = instructions[0].resource - - // add target of reference to function if reference is not null - val structureMapFunctionHead = "group Extract$groupName(source src : QuestionniareResponse, target bundle: Bundle) {" - stringBuilder.appendNewLine() - stringBuilder.append(structureMapFunctionHead) - .appendNewLine() - stringBuilder.append("src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {") - .appendNewLine() - - val mainNest = Nest() - mainNest.fullPath = "" - mainNest.name = "" - mainNest.resourceName = resourceName - - instructions.forEachIndexed { index, instruction -> - mainNest.add(instruction) - } - - mainNest.buildStructureMap(0, questionnaireResponse) - - - stringBuilder.append("} ") - addRuleNo() - stringBuilder.appendNewLine() - stringBuilder.append("}") - stringBuilder.appendNewLine() - stringBuilder.appendNewLine() - } else{ - println("$groupName is not a valid hl7 resource name") + } + + fun addRuleNo() { + stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) + } + + fun Instruction.getPropertyPath(): String { + return questionsPath.getOrDefault(responseFieldId, "") + } + + fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { + // 1. If the answer is static/literal, just return it here + // TODO: We should infer the resource element and add the correct conversion or code to assign + // this correctly + if (constantValue != null) { + return when { + fieldPath == "id" -> "create('id') as id, id.value = '$constantValue'" + fieldPath == "rank" -> { + val constValue = constantValue!!.replace(".0", "") + "create('positiveInt') as rank, rank.value = '$constValue'" } + else -> "'$constantValue'" + } } - fun addRuleNo() { - stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) + // 2. If the answer is from the QuestionnaireResponse, get the ID of the item in the + // "Questionnaire Response Field Id" and + // get its value using FHIR Path expressions + if (responseFieldId != null) { + // TODO: Fix the 1st param inside the evaluate expression + var expression = + "${"$"}this.item${getPropertyPath()}.where(linkId = '$responseFieldId').answer.value" + // TODO: Fix these to use infer + if (fieldPath == "id" || fieldPath == "rank") { + expression = + "create('${if (fieldPath == "id") "id" else "positiveInt"}') as $fieldPath, $fieldPath.value = evaluate(src, $expression)" + } else { + // TODO: Infer the resource property type and answer to perform other conversions + // TODO: Extend this to cover other corner cases + if (expression.isCoding(questionnaireResponse) && fieldPath.isEnumeration(this)) { + expression = expression.replace("answer.value", "answer.value.code") + } else if (inferType(fullPropertyPath()) == "CodeableConcept") { + return "''" + } + expression = "evaluate(src, $expression)" + } + return expression } - fun Instruction.getPropertyPath(): String { - return questionsPath.getOrDefault(responseFieldId, "") + // 3. If it's a FHIR Path/StructureMap function, add the contents directly from here to the + // StructureMap + if (fhirPathStructureMapFunctions != null && fhirPathStructureMapFunctions!!.isNotEmpty()) { + // TODO: Fix the 2nd param inside the evaluate expression --> Not sure what this is but check + // this + return fhirPathStructureMapFunctions!! } - - fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { - - //1. If the answer is static/literal, just return it here - // TODO: We should infer the resource element and add the correct conversion or code to assign this correctly - if (constantValue != null) { - return when { - fieldPath == "id" -> "create('id') as id, id.value = '$constantValue'" - fieldPath == "rank" -> { - val constValue = constantValue!!.replace(".0", "") - "create('positiveInt') as rank, rank.value = '$constValue'" - } - else -> "'$constantValue'" - } - } - - // 2. If the answer is from the QuestionnaireResponse, get the ID of the item in the "Questionnaire Response Field Id" and - // get its value using FHIR Path expressions - if (responseFieldId != null) { - // TODO: Fix the 1st param inside the evaluate expression - var expression = "${"$"}this.item${getPropertyPath()}.where(linkId = '$responseFieldId').answer.value" - // TODO: Fix these to use infer - if (fieldPath == "id" || fieldPath == "rank") { - expression = "create('${if (fieldPath == "id") "id" else "positiveInt"}') as $fieldPath, $fieldPath.value = evaluate(src, $expression)" - } else { - - // TODO: Infer the resource property type and answer to perform other conversions - // TODO: Extend this to cover other corner cases - if (expression.isCoding(questionnaireResponse) && fieldPath.isEnumeration(this)) { - expression = expression.replace("answer.value", "answer.value.code") - } else if (inferType(fullPropertyPath()) == "CodeableConcept") { - return "''" - } - expression = "evaluate(src, $expression)" - } - return expression - } - - // 3. If it's a FHIR Path/StructureMap function, add the contents directly from here to the StructureMap - if (fhirPathStructureMapFunctions != null && fhirPathStructureMapFunctions!!.isNotEmpty()) { - // TODO: Fix the 2nd param inside the evaluate expression --> Not sure what this is but check this - return fhirPathStructureMapFunctions!! - } - // 4. If the answer is a conversion, (Assume this means it's being converted to a reference) - if (conversion != null && conversion!!.isNotBlank() && conversion!!.isNotEmpty()) { - println("current resource to reference is $conversion") - - val resourceName = conversion!!.replace("$", "") - var resourceIndex = conversion!!.replace("$$resourceName", "") - if (resourceIndex.isNotEmpty()) { - resourceIndex = "[$resourceIndex]" - } - val reference = generateReference(resourceName = resourceName, resourceIndex = resourceIndex) - return reference - } - - /* - 5. You can use $Resource eg $Patient to reference another resource being extracted here, - but how do we actually get its instance so that we can use it???? - This should be handled elsewhere - */ - - return "''" + // 4. If the answer is a conversion, (Assume this means it's being converted to a reference) + if (conversion != null && conversion!!.isNotBlank() && conversion!!.isNotEmpty()) { + println("current resource to reference is $conversion") + + val resourceName = conversion!!.replace("$", "") + var resourceIndex = conversion!!.replace("$$resourceName", "") + if (resourceIndex.isNotEmpty()) { + resourceIndex = "[$resourceIndex]" + } + val reference = generateReference(resourceName = resourceName, resourceIndex = resourceIndex) + return reference } + /* + 5. You can use $Resource eg $Patient to reference another resource being extracted here, + but how do we actually get its instance so that we can use it???? - This should be handled elsewhere + */ + + return "''" + } + + inner class Nest { + var instruction: Instruction? = null + + // We can change this to a linked list + val nests = ArrayList() + lateinit var name: String + lateinit var fullPath: String + lateinit var resourceName: String + + fun add(instruction: Instruction) { + /*if (instruction.fieldPath.startsWith(fullPath)) { + + }*/ + val remainingPath = instruction.fieldPath.replace(fullPath, "") + + remainingPath.run { + if (contains(".")) { + val parts = split(".") + val partName = parts[0].ifEmpty { parts[1] } + + // Search for the correct property to put this nested property + nests.forEach { + if (partName.startsWith(it.name)) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + if (index > 0 && index < parts.size - 1) { + newFieldPath += "." + } + } - inner class Nest { - var instruction: Instruction? = null - - // We can change this to a linked list - val nests = ArrayList() - lateinit var name: String - lateinit var fullPath: String - lateinit var resourceName: String - - fun add(instruction: Instruction) { - /*if (instruction.fieldPath.startsWith(fullPath)) { + fieldPath = newFieldPath + } - }*/ - val remainingPath = instruction.fieldPath.replace(fullPath, "") + it.add(nextInstruction) - remainingPath.run { - if (contains(".")) { - val parts = split(".") - val partName = parts[0].ifEmpty { - parts[1] - } + return@run + } + } - // Search for the correct property to put this nested property - nests.forEach { - if (partName.startsWith(it.name)) { - val nextInstruction = Instruction().apply { - copyFrom(instruction) - var newFieldPath = "" - parts.forEachIndexed { index, s -> - if (index != 0) { - newFieldPath += s - } - - if (index > 0 && index < parts.size - 1) { - newFieldPath += "." - } - } - - fieldPath = newFieldPath - } - - it.add(nextInstruction) - - return@run - } - } + // If no match is found, let's create a new one + val newNest = + Nest().apply { + name = partName - // If no match is found, let's create a new one - val newNest = Nest().apply { - name = partName - - fullPath = if (this@Nest.fullPath.isNotEmpty()) { - "${this@Nest.fullPath}.$partName" - } else { - partName - } - resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" - - if ((parts[0].isEmpty() && parts.size > 2) || (parts[0].isNotEmpty() && parts.size > 1)) { - val nextInstruction = Instruction().apply { - copyFrom(instruction) - var newFieldPath = "" - parts.forEachIndexed { index, s -> - if (index != 0) { - newFieldPath += s - } - } - - fieldPath = newFieldPath - } - add(nextInstruction) - } else { - this@apply.instruction = instruction - } - } - nests.add(newNest) + fullPath = + if (this@Nest.fullPath.isNotEmpty()) { + "${this@Nest.fullPath}.$partName" } else { - this@Nest.nests.add(Nest().apply { - name = remainingPath - fullPath = instruction.fieldPath - this@apply.instruction = instruction - resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" - }) + partName } + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + + if ( + (parts[0].isEmpty() && parts.size > 2) || (parts[0].isNotEmpty() && parts.size > 1) + ) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + } + + fieldPath = newFieldPath + } + add(nextInstruction) + } else { + this@apply.instruction = instruction + } } + nests.add(newNest) + } else { + this@Nest.nests.add( + Nest().apply { + name = remainingPath + fullPath = instruction.fieldPath + this@apply.instruction = instruction + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + }, + ) } + } + } - fun buildStructureMap(currLevel: Int, questionnaireResponse: QuestionnaireResponse) { - if (instruction != null) { - val answerExpression = instruction?.getAnswerExpression(questionnaireResponse) - - if (answerExpression != null) { - if (answerExpression.isNotEmpty() && answerExpression.isNotBlank() && answerExpression != "''") { - val propertyType = inferType(instruction!!.fullPropertyPath()) - val answerType = answerExpression.getAnswerType(questionnaireResponse) - - if (propertyType != "Type" && answerType != propertyType && propertyType?.canHandleConversion( - answerType ?: "" - )?.not() == true && answerExpression.startsWith("evaluate") - ) { - println("Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType") - stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") - stringBuilder.append("create('${propertyType.getFhirType()}') as randomVal, randomVal.value = ") - stringBuilder.append(answerExpression) - addRuleNo() - stringBuilder.appendNewLine() - return - } - - stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") - stringBuilder.append(answerExpression) - addRuleNo() - stringBuilder.appendNewLine() - } - } - } else if (nests.size > 0) { - //val resourceType = inferType("entity$currLevel.$name", instruction) + fun buildStructureMap(currLevel: Int, questionnaireResponse: QuestionnaireResponse) { + if (instruction != null) { + val answerExpression = instruction?.getAnswerExpression(questionnaireResponse) + + if (answerExpression != null) { + if ( + answerExpression.isNotEmpty() && + answerExpression.isNotBlank() && + answerExpression != "''" + ) { + val propertyType = inferType(instruction!!.fullPropertyPath()) + val answerType = answerExpression.getAnswerType(questionnaireResponse) + + if ( + propertyType != "Type" && + answerType != propertyType && + propertyType + ?.canHandleConversion( + answerType ?: "", + ) + ?.not() == true && + answerExpression.startsWith("evaluate") + ) { + println( + "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType" + ) + stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") + stringBuilder.append( + "create('${propertyType.getFhirType()}') as randomVal, randomVal.value = " + ) + stringBuilder.append(answerExpression) + addRuleNo() + stringBuilder.appendNewLine() + return + } - if (!name.equals("")) { - val resourceType = resourceName - stringBuilder.append("src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {") - stringBuilder.appendNewLine() - } else { - //stringBuilder.append("src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {") + stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") + stringBuilder.append(answerExpression) + addRuleNo() + stringBuilder.appendNewLine() + } + } + } else if (nests.size > 0) { + // val resourceType = inferType("entity$currLevel.$name", instruction) + + if (!name.equals("")) { + val resourceType = resourceName + stringBuilder.append( + "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {" + ) + stringBuilder.appendNewLine() + } else { + // stringBuilder.append("src -> entity$currLevel.$name = create('$resourceType') as + // entity${currLevel + 1} then {") + } - } + nests.forEach { it.buildStructureMap(currLevel + 1, questionnaireResponse) } - nests.forEach { - it.buildStructureMap(currLevel + 1, questionnaireResponse) - } + // nest!!.buildStructureMap(currLevel + 1) - //nest!!.buildStructureMap(currLevel + 1) - - if (!name.equals("")) { - stringBuilder.append("}") - addRuleNo() - } else { - //addRuleNo() - } - stringBuilder.appendNewLine() - } else { - throw Exception("nest & instruction are null inside Nest object") - } + if (!name.equals("")) { + stringBuilder.append("}") + addRuleNo() + } else { + // addRuleNo() } + stringBuilder.appendNewLine() + } else { + throw Exception("nest & instruction are null inside Nest object") + } } - + } } fun generateStructureMapLine( - structureMapBody: StringBuilder, - row: Row, - resource: Resource, - extractionResources: HashMap + structureMapBody: StringBuilder, + row: Row, + resource: Resource, + extractionResources: HashMap, ) { - row.forEachIndexed { index, cell -> - val cellValue = cell.stringCellValue - val fieldPath = row.getCell(4).stringCellValue - val targetDataType = determineFhirDataType(cellValue) - structureMapBody.append("src -> entity.${fieldPath}=") - - when (targetDataType) { - "string" -> { - structureMapBody.append("create('string').value ='$cellValue'") - } - - "integer" -> { - structureMapBody.append("create('integer').value = $cellValue") - } - - "boolean" -> { - val booleanValue = - if (cellValue.equals("true", ignoreCase = true)) "true" else "false" - structureMapBody.append("create('boolean').value = $booleanValue") - } - - else -> { - structureMapBody.append("create('unsupportedDataType').value = '$cellValue'") - } - } - structureMapBody.appendNewLine() + row.forEachIndexed { index, cell -> + val cellValue = cell.stringCellValue + val fieldPath = row.getCell(4).stringCellValue + val targetDataType = determineFhirDataType(cellValue) + structureMapBody.append("src -> entity.$fieldPath=") + + when (targetDataType) { + "string" -> { + structureMapBody.append("create('string').value ='$cellValue'") + } + "integer" -> { + structureMapBody.append("create('integer').value = $cellValue") + } + "boolean" -> { + val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" + structureMapBody.append("create('boolean').value = $booleanValue") + } + else -> { + structureMapBody.append("create('unsupportedDataType').value = '$cellValue'") + } } + structureMapBody.appendNewLine() + } } fun determineFhirDataType(cellValue: String): String { - val cleanedValue = cellValue.trim().toLowerCase() - - when { - cleanedValue == "true" || cleanedValue == "false" -> return "boolean" - cleanedValue.matches(Regex("-?\\d+")) -> return "boolean" - cleanedValue.matches(Regex("-?\\d*\\.\\d+")) -> return "decimal" - cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}")) -> return "date" - cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")) -> return "dateTime" - else -> { - return "string" - } + val cleanedValue = cellValue.trim().toLowerCase() + + when { + cleanedValue == "true" || cleanedValue == "false" -> return "boolean" + cleanedValue.matches(Regex("-?\\d+")) -> return "boolean" + cleanedValue.matches(Regex("-?\\d*\\.\\d+")) -> return "decimal" + cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}")) -> return "date" + cleanedValue.matches(Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")) -> return "dateTime" + else -> { + return "string" } + } } fun StringBuilder.appendNewLine(): StringBuilder { - append(System.lineSeparator()) - return this + append(System.lineSeparator()) + return this } - private val Field.isList: Boolean - get() = isParameterized && type == List::class.java + get() = isParameterized && type == List::class.java private val Field.isParameterized: Boolean - get() = genericType is ParameterizedType + get() = genericType is ParameterizedType /** The non-parameterized type of this field (e.g. `String` for a field of type `List`). */ private val Field.nonParameterizedType: Class<*> - get() = - if (isParameterized) (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> - else type + get() = + if (isParameterized) { + (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + } else { + type + } private fun Class<*>.getFieldOrNull(name: String): Field? { - return try { - getDeclaredField(name) - } catch (ex: NoSuchFieldException) { - superclass?.getFieldOrNull(name) - } + return try { + getDeclaredField(name) + } catch (ex: NoSuchFieldException) { + superclass?.getFieldOrNull(name) + } } private fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { - val answerType = getType(questionnaireResponse) - return if (answerType != null) { - answerType == "org.hl7.fhir.r4.model.Coding" - } else { - false - } + val answerType = getType(questionnaireResponse) + return if (answerType != null) { + answerType == "org.hl7.fhir.r4.model.Coding" + } else { + false + } } private fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { - val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) - return answer.firstOrNull()?.javaClass?.name + return answer.firstOrNull()?.javaClass?.name } - internal val fhirPathEngine: FHIRPathEngine = - with(FhirContext.forCached(FhirVersionEnum.R4)) { - FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { - hostServices = FHIRPathEngineHostServices - } + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + hostServices = FHIRPathEngineHostServices } + } private fun String.isEnumeration(instruction: Instruction): Boolean { - return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false + return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false } - fun String.getAnswerType(questionnaireResponse: QuestionnaireResponse): String? { - return if (isEvaluateExpression()) { - val fhirPath = substring(indexOf(",") + 1, length - 1) - - fhirPath.getType(questionnaireResponse) - ?.replace("org.hl7.fhir.r4.model.", "") - } else { - // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual one that is generated and confirm if we need more conversions - "StringType"; - } + return if (isEvaluateExpression()) { + val fhirPath = substring(indexOf(",") + 1, length - 1) + + fhirPath.getType(questionnaireResponse)?.replace("org.hl7.fhir.r4.model.", "") + } else { + // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual + // one that is generated and confirm if we need more conversions + "StringType" + } } // TODO: Confirm and fix this fun String.isEvaluateExpression(): Boolean = startsWith("evaluate(") - /** * Infer's the type and return the short class name eg `HumanName` for org.fhir.hl7.r4.model.Patient * when given the path `Patient.name` */ fun inferType(propertyPath: String): String? { - // TODO: Handle possible errors - // TODO: Handle inferring nested types - val parts = propertyPath.split(".") - val parentResourceClassName = parts[0] - lateinit var parentClass: Class<*> - - if (fhirResources.contains(parentResourceClassName)) { - parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") - return inferType(parentClass, parts, 1) - } else { - return null - } + // TODO: Handle possible errors + // TODO: Handle inferring nested types + val parts = propertyPath.split(".") + val parentResourceClassName = parts[0] + lateinit var parentClass: Class<*> + + if (fhirResources.contains(parentResourceClassName)) { + parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") + return inferType(parentClass, parts, 1) + } else { + return null + } } fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? { - val resourcePropertyName = parts[index] - val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) - - val propertyType = if (propertyField?.isList == true) - propertyField.nonParameterizedType - // TODO: Check if this is required - else if (propertyField?.type == Enumeration::class.java) - // TODO: Check if this works - propertyField.nonParameterizedType - else - propertyField?.type - - return if (parts.size > index + 1) { - return inferType(propertyType, parts, index + 1) - } else - propertyType?.name - ?.replace("org.hl7.fhir.r4.model.", "") + val resourcePropertyName = parts[index] + val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) + + val propertyType = + if (propertyField?.isList == true) { + propertyField.nonParameterizedType + } // TODO: Check if this is required + else if (propertyField?.type == Enumeration::class.java) { + // TODO: Check if this works + propertyField.nonParameterizedType + } else { + propertyField?.type + } + + return if (parts.size > index + 1) { + return inferType(propertyType, parts, index + 1) + } else { + propertyType?.name?.replace("org.hl7.fhir.r4.model.", "") + } } fun String.isMultipleTypes(): Boolean = this == "Type" // TODO: Finish this. Use the annotation @Chid.type fun String.getPossibleTypes(): List { - return listOf() + return listOf() } - fun String.canHandleConversion(sourceType: String): Boolean { - val propertyClass = Class.forName("org.hl7.fhir.r4.model.$this") - val targetType2 = - if (sourceType == "StringType") String::class.java else Class.forName("org.hl7.fhir.r4.model.$sourceType") - - val possibleConversions = listOf( - "BooleanType" to "StringType", - "DateType" to "StringType", - "DecimalType" to "IntegerType", - "AdministrativeGender" to "CodeType" + val propertyClass = Class.forName("org.hl7.fhir.r4.model.$this") + val targetType2 = + if (sourceType == "StringType") String::class.java + else Class.forName("org.hl7.fhir.r4.model.$sourceType") + + val possibleConversions = + listOf( + "BooleanType" to "StringType", + "DateType" to "StringType", + "DecimalType" to "IntegerType", + "AdministrativeGender" to "CodeType", ) - possibleConversions.forEach { - if (this.contains(it.first) && sourceType == it.second) { - return true - } + possibleConversions.forEach { + if (this.contains(it.first) && sourceType == it.second) { + return true } + } - try { - propertyClass.getDeclaredMethod("fromCode", targetType2) - } catch (ex: NoSuchMethodException) { - return false - } + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } - return true + return true } fun String.getParentResource(): String? { - return substring(0, lastIndexOf('.')) + return substring(0, lastIndexOf('.')) } - fun String.getResourceProperty(): String? { - return substring(lastIndexOf('.') + 1) + return substring(lastIndexOf('.') + 1) } -fun String.getFhirType(): String = replace("Type", "") - .lowercase() \ No newline at end of file +fun String.getFhirType(): String = replace("Type", "").lowercase() diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt index 6f3a3da7..c1146e87 100644 --- a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt @@ -1,80 +1,96 @@ package org.smartregister.fhir.structuremaptool import org.hl7.fhir.r4.model.StringType -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -class FHIRPathEngineHostServicesTest { +class FhirPathEngineHostServicesTest { - @Test - fun testResolveConstant() { - val appContext = mapOf("test" to StringType("Test Value")) - val result = FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + @Test + fun testResolveConstant() { + val appContext = mapOf("test" to StringType("Test Value")) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - assertNotNull(result) - assertEquals("Test Value", (result as StringType).value) - } + assertNotNull(result) + assertEquals("Test Value", (result as StringType).value) + } - @Test - fun testResolveConstant_cache() { - // Set up the application context with a constant value - val appContext = mapOf("test" to StringType("Test Value")) + @Test + fun testResolveConstant_cache() { + // Set up the application context with a constant value + val appContext = mapOf("test" to StringType("Test Value")) - // First call: Resolves and caches the constant value - FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + // First call: Resolves and caches the constant value + FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - // Second call: Should retrieve the value from the cache - val result = FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + // Second call: Should retrieve the value from the cache + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - // Verify that the result is not null and matches the expected value - assertNotNull(result, "The resolved constant should not be null") - assertEquals("Test Value", (result as StringType).value, "The resolved constant should match the expected value") - } + // Verify that the result is not null and matches the expected value + assertNotNull(result, "The resolved constant should not be null") + assertEquals( + "Test Value", + (result as StringType).value, + "The resolved constant should match the expected value" + ) + } + @Test + fun testLog() { + val logResult = FhirPathEngineHostServices.log("Test log message", mutableListOf()) + assertTrue(logResult) + } - @Test - fun testLog() { - val logResult = FHIRPathEngineHostServices.log("Test log message", mutableListOf()) - assertTrue(logResult) - } + @Test + fun testResolveFunction_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveFunction("testFunction") + } + assertEquals("Function testFunction is not yet implemented.", exception.message) + } - @Test - fun testResolveFunction_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveFunction("testFunction") - } - assertEquals("Function testFunction is not yet implemented.", exception.message) - } + @Test + fun testResolveValueSet_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveValueSet(null, "http://example.com") + } + assertEquals("resolveValueSet is not yet implemented.", exception.message) + } - @Test - fun testResolveValueSet_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveValueSet(null, "http://example.com") - } - assertEquals("resolveValueSet is not yet implemented.", exception.message) - } + @Test + fun testResolveReference_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveReference(null, "http://example.com") + } + assertEquals("resolveReference is not yet implemented.", exception.message) + } - @Test - fun testResolveReference_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveReference(null, "http://example.com") - } - assertEquals("resolveReference is not yet implemented.", exception.message) - } + @Test + fun testCheckFunctionThrowsUnsupportedOperationException() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) + } + assertEquals("checkFunction is not yet implemented.", exception.message) + } - @Test - fun testCheckFunctionThrowsUnsupportedOperationException() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) - } - assertEquals("checkFunction is not yet implemented.", exception.message) - } - - @Test - fun testExecuteFunctionThrowsUnsupportedException() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.executeFunction(null, mutableListOf(), "testFunction", mutableListOf()) - } - assertEquals("executeFunction is not yet implemented.", exception.message) - } + @Test + fun testExecuteFunctionThrowsUnsupportedException() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.executeFunction( + null, + mutableListOf(), + "testFunction", + mutableListOf() + ) + } + assertEquals("executeFunction is not yet implemented.", exception.message) + } } From 8bc646e8ff448d737f24cf341fcadb76e9556c8a Mon Sep 17 00:00:00 2001 From: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:44:06 +0300 Subject: [PATCH 2/4] Test transform support services. (#282) Signed-off-by: Lentumunai-Mark Co-authored-by: Peter Lubell-Doughtie --- sm-gen/build.gradle.kts | 23 +-- .../TransformSupportServices.kt | 38 ++--- .../TransformSupportServicesTest.kt | 137 ++++++++++++++++++ 3 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt diff --git a/sm-gen/build.gradle.kts b/sm-gen/build.gradle.kts index dc3a3276..284ed683 100644 --- a/sm-gen/build.gradle.kts +++ b/sm-gen/build.gradle.kts @@ -11,18 +11,23 @@ group = "org.example" version = "1.0-SNAPSHOT" repositories { - mavenCentral() - gradlePluginPortal() + google() + mavenCentral() + mavenLocal() } dependencies { - testImplementation(kotlin("test")) - implementation("com.github.ajalt.clikt:clikt:3.4.0") - implementation("org.apache.poi:poi:3.17") - implementation("org.apache.poi:poi-ooxml:3.17") - implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.4.0") - implementation("ca.uhn.hapi.fhir:hapi-fhir-validation:5.4.0") - implementation(kotlin("stdlib-jdk8")) + + implementation("com.github.ajalt.clikt:clikt:3.4.0") + implementation("org.apache.poi:poi:3.17") + implementation("org.apache.poi:poi-ooxml:3.17") + implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.4.0") + implementation("ca.uhn.hapi.fhir:hapi-fhir-validation:5.4.0") + implementation(kotlin("stdlib-jdk8")) + testImplementation(kotlin("test")) + testImplementation("io.mockk:mockk:1.13.7") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") } tasks.test { useJUnitPlatform() } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt index e638a22b..36403e0a 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt @@ -27,25 +27,25 @@ class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorker println(message) } - @Throws(FHIRException::class) - override fun createType(appInfo: Any, name: String): Base { - return when (name) { - "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() - "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() - "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() - "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() - "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() - "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() - "Encounter_Participant" -> Encounter.EncounterParticipantComponent() - "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() - "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() - "Patient_Link" -> Patient.PatientLinkComponent() - "Timing_Repeat" -> Timing.TimingRepeatComponent() - "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() - "Group_Characteristic" -> Group.GroupCharacteristicComponent() - "Observation_Component" -> Observation.ObservationComponentComponent() - else -> ResourceFactory.createResourceOrType(name) - } + @Throws(FHIRException::class) + override fun createType(appInfo: Any, name: String): Base { + return when (name) { + "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() + "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() + "Immunization_AppliedProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() + "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() + "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() + "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() + "Encounter_Participant" -> Encounter.EncounterParticipantComponent() + "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() + "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() + "Patient_Link" -> Patient.PatientLinkComponent() + "Timing_Repeat" -> Timing.TimingRepeatComponent() + "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() + "Group_Characteristic" -> Group.GroupCharacteristicComponent() + "Observation_Component" -> Observation.ObservationComponentComponent() + else -> ResourceFactory.createResourceOrType(name) + } } override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt new file mode 100644 index 00000000..6d4f460f --- /dev/null +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt @@ -0,0 +1,137 @@ +package org.smartregister.fhir.structuremaptool + +import io.mockk.mockk +import org.hl7.fhir.exceptions.FHIRException +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.EpisodeOfCare +import org.hl7.fhir.r4.model.Immunization +import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.RiskAssessment +import org.hl7.fhir.r4.model.TimeType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import kotlin.test.Test + +class TransformSupportServicesTest{ + lateinit var transformSupportServices: TransformSupportServices + + @BeforeEach + fun setUp() { + transformSupportServices = TransformSupportServices(mockk()) + } + + + @Test + fun `createType() should return RiskAssessmentPrediction when given RiskAssessment_Prediction`() { + assertTrue( + transformSupportServices.createType("", "RiskAssessment_Prediction") + is RiskAssessment.RiskAssessmentPredictionComponent, + ) + } + + @Test + fun `createType() should return ImmunizationProtocol when given Immunization_VaccinationProtocol`() { + assertTrue( + transformSupportServices.createType("", "Immunization_AppliedProtocol") + is Immunization.ImmunizationProtocolAppliedComponent, + ) + } + + @Test + fun `createType() should return ImmunizationReaction when given Immunization_Reaction`() { + assertTrue( + transformSupportServices.createType("", "Immunization_Reaction") + is Immunization.ImmunizationReactionComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given EpisodeOfCare_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "EpisodeOfCare_Diagnosis") + is EpisodeOfCare.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given Encounter_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Diagnosis") + is Encounter.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return EncounterParticipant when given Encounter_Participant`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Participant") + is Encounter.EncounterParticipantComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivity when given CarePlan_Activity`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_Activity") + is CarePlan.CarePlanActivityComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivityDetail when given CarePlan_ActivityDetail`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_ActivityDetail") + is CarePlan.CarePlanActivityDetailComponent, + ) + } + + @Test + fun `createType() should return PatientLink when given Patient_Link`() { + assertTrue( + transformSupportServices.createType("", "Patient_Link") is Patient.PatientLinkComponent, + ) + } + + @Test + fun `createType() should return ObservationComponentComponent when given Observation_Component`() { + assertTrue( + transformSupportServices.createType("", "Observation_Component") + is Observation.ObservationComponentComponent, + ) + } + + @Test + fun `createType() should return Time when given time`() { + assertTrue(transformSupportServices.createType("", "time") is TimeType) + } + + @Test + fun `createResource() should add resource into output when given Patient and atRootOfTransForm as True`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), true) + assertEquals(transformSupportServices.outputs.size, 1) + } + + @Test + fun `createResource() should not add resource into output when given Patient and atRootOfTransForm as False`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), false) + assertEquals(transformSupportServices.outputs.size, 0) + } + + @Test + fun `resolveReference should throw FHIRException when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.resolveReference("", "https://url.com") + } + } + + @Test + fun `performSearch() should throw FHIRException this is not supported yet when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.performSearch("", "https://url.com") + } + } +} \ No newline at end of file From fefe0b500e42cec792a694a4ef605e49cb638602 Mon Sep 17 00:00:00 2001 From: Benjamin Mwalimu Date: Fri, 13 Sep 2024 17:56:28 +0300 Subject: [PATCH 3/4] -Add new OpenSRP web roles (#286) --- importer/csv/setup/roles.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/importer/csv/setup/roles.csv b/importer/csv/setup/roles.csv index aff12ebd..6a33ca39 100644 --- a/importer/csv/setup/roles.csv +++ b/importer/csv/setup/roles.csv @@ -118,3 +118,5 @@ WEB_CLIENT,, ANDROID_CLIENT,, EDIT_KEYCLOAK_USERS,TRUE,manage-users|query-users VIEW_KEYCLOAK_USERS,TRUE,view-users|query-users|query-groups +VIEW_USER_GROUPS,, +VIEW_ROLES,, \ No newline at end of file From 089700048f629108510916f7c83da5cab853080b Mon Sep 17 00:00:00 2001 From: Benjamin Mwalimu Date: Tue, 17 Sep 2024 16:24:58 +0300 Subject: [PATCH 4/4] Update README.md (#287) --- importer/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importer/README.md b/importer/README.md index e9b9a83a..970239c3 100644 --- a/importer/README.md +++ b/importer/README.md @@ -1,6 +1,6 @@ # Setup Keycloak Roles -This script is used to setup keycloak roles and groups. It takes in a csv file with the following columns: +This script is used to set up keycloak roles and groups. It takes in a CSV file with the following columns: - **role**: The actual names of the roles you would like to create - **composite**: A boolean value that tells if the role has composite roles or not @@ -8,19 +8,19 @@ This script is used to setup keycloak roles and groups. It takes in a csv file w ### Options -- `setup` : (Required) This needs to be set to "roles" in order to initiate the setup process -- `csv_file` : (Required) The csv file with the list of roles +- `setup` : (Required) This needs to be set to "roles" to initiate the setup process +- `csv_file` : (Required) The CSV file with the list of roles - `group` : (Not required) This is the actual group name. If not passed then the roles will just be created but not assigned to any group -- `roles_max` : (Not required) This is the maximum number of roles to pull from the api. The default is set to 500. If the number of roles in your setup is more than this you will need to change this value +- `roles_max` : (Not required) This is the maximum number of roles to pull from the API. The default is set to 500. If the number of roles in your setup is more than this you will need to change this value - `default_groups` : (Not Required) This is a boolean value to turn on and off the assignment of default roles. The default value is `true` ### To run script 1. Create virtualenv 2. Install requirements.txt - `pip install -r requirements.txt` -3. Set up your _.env_ file, see sample below. Populate it with the right credentials, you can either provide an access token or client credentials. Ensure that the user whose details you provide in this _.env_ file has the necessary permissions/privileges. +3. Set up your _.env_ file, see the sample below. Populate it with the right credentials, you can either provide an access token or client credentials. Ensure the user whose details you provide in this _.env_ file has the necessary permissions/privileges. 4. Run script - `python3 main.py --setup roles --csv_file csv/setup/roles.csv --group Supervisor` -5. If you are running the script without `https` setup e.g locally or a server without https setup, you will need to set the `OAUTHLIB_INSECURE_TRANSPORT` environment variable to 1. For example `export OAUTHLIB_INSECURE_TRANSPORT=1 && python3 main.py --setup roles --csv_file csv/setup/roles.csv --group OpenSRP_Provider --log_level debug` +5. If you are running the script without `https` setup e.g. locally or a server without https setup, you will need to set the `OAUTHLIB_INSECURE_TRANSPORT` environment variable to 1. For example `export OAUTHLIB_INSECURE_TRANSPORT=1 && python3 main.py --setup roles --csv_file csv/setup/roles.csv --group OpenSRP_Provider --log_level debug` 6. You can turn on logging by passing a `--log_level` to the command line as `info`, `debug` or `error`. For example `python3 main.py --setup roles --csv_file csv/setup/roles.csv --group Supervisor --log_level debug` @@ -30,6 +30,7 @@ client_id = 'example-client-id' client_secret = 'example-client-secret' fhir_base_url = 'https://example.smartregister.org/fhir' keycloak_url = 'https://keycloak.smartregister.org/auth' +realm = 'example-realm' # access token for access to where product images are remotely stored product_access_token = 'example-product-access-token'