From a13a1ff833429096d6afa479d3c0b0e102ee0da2 Mon Sep 17 00:00:00 2001 From: Wambere Date: Fri, 21 Jun 2024 12:23:07 +0300 Subject: [PATCH 01/10] Initial commit to add sm generation tool to efsity --- efsity/build.gradle.kts | 2 + .../src/main/java/org/smartregister/Main.java | 3 +- .../command/GenerateStructureMapCommand.java | 48 ++ .../GenerateStructureMapService.kt | 274 +++++++++ .../TransformSupportServices.kt | 72 +++ .../smartregister/structuremaptool/Utils.kt | 527 ++++++++++++++++++ 6 files changed, 925 insertions(+), 1 deletion(-) create mode 100644 efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java create mode 100644 efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt create mode 100644 efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt create mode 100644 efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index f1224b04..0fd2e377 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -82,6 +82,8 @@ dependencies { implementation(deps.jsonschemafriend) implementation(deps.picocli) implementation(deps.xstream) + implementation("org.apache.poi:poi:4.1.1") + implementation("org.apache.poi:poi-ooxml:4.1.1") testImplementation(kotlin("test")) testImplementation("junit:junit:4.13.2") diff --git a/efsity/src/main/java/org/smartregister/Main.java b/efsity/src/main/java/org/smartregister/Main.java index 679d1dbb..6b5f3673 100644 --- a/efsity/src/main/java/org/smartregister/Main.java +++ b/efsity/src/main/java/org/smartregister/Main.java @@ -21,7 +21,8 @@ TranslateCommand.class, QuestionnaireResponseGeneratorCommand.class, ValidateFileStructureCommand.class, - PublishFhirResourcesCommand.class + PublishFhirResourcesCommand.class, + GenerateStructureMapCommand.class }) public class Main implements Runnable { public static void main(String[] args) { diff --git a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java new file mode 100644 index 00000000..a3c15b0c --- /dev/null +++ b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java @@ -0,0 +1,48 @@ +package org.smartregister.command; + +import org.smartregister.structuremaptool.GenerateStructureMapServiceKt; +import org.smartregister.util.FctUtils; +import picocli.CommandLine; + +import java.io.IOException; + +@CommandLine.Command(name = "generateStructureMap") +public class GenerateStructureMapCommand implements Runnable { + @CommandLine.Option( + names = {"-q", "--questionnaire"}, + description = "Questionnaire", + required = true) + private String questionnairePath; + + @CommandLine.Option( + names = {"-c", "--configPath"}, + description = "StructureMap generation configuration in an excel sheet", + required = true) + private String configPath; + + @CommandLine.Option( + names = {"-qr", "--questionnaireResponsePath"}, + description = "Questionnaire response", + required = true) + private String questionnaireResponsePath; + + @Override + public void run(){ + if (configPath != null){ + try { + generateStructureMap(configPath, questionnairePath, questionnaireResponsePath); + } catch (IOException e){ + throw new RuntimeException(e); + } + } + } + + public static void generateStructureMap( + String configPath, String questionnairePath, String questionnaireResponsePath) throws IOException{ + long start = System.currentTimeMillis(); + FctUtils.printInfo("Starting StructureMap generation"); + GenerateStructureMapServiceKt.main(configPath, questionnairePath, questionnaireResponsePath); + + FctUtils.printCompletedInDuration(start); + } +} diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt new file mode 100644 index 00000000..640449d8 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt @@ -0,0 +1,274 @@ +package org.smartregister.structuremaptool + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.FhirVersionEnum +import ca.uhn.fhir.parser.IParser +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.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.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( + xlsConfigFilePath: String, + questionnaireFilePath: String, + questionnaireResponsePath: String +) { + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire : Questionnaire = fhirJsonParser.parseResource( + Questionnaire::class.java, FileUtils.readFileToString(File(questionnaireFilePath), Charset.defaultCharset())) + val questionnaireResponse: QuestionnaireResponse = fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, FileUtils.readFileToString(File(questionnaireResponsePath), Charset.defaultCharset())) + + // reads the xls + val xlsConfigFile = FileInputStream(xlsConfigFilePath) + val xlWorkbook = WorkbookFactory.create(xlsConfigFile) + + // 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 = xlWorkbook.getSheet("Settings") + var questionnaireId : String? = null + + 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 + } + } + + /* + 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() + + 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() + + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWorkbook.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(2) + sb.append(structureMapBody) + + // Fix the question 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(3) + + 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 String.clean() : String { + return this.replace("-", "") + .replace("_", "") + .replace(" ", "") +} + +fun Row.isEmpty() : Boolean { + return getCell(0) == null && getCell(1) == null && getCell(2) == null +} + +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.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 + } +} + +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 + + // TODO: Clean the following properties + var fullFieldPath = "" + fun fullPropertyPath() : String = "$resource.$fullFieldPath" + + fun searchKey() = resource + resourceIndex +} + +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() +} + +fun String.addIndentation(times: Int) : String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } + + processedString += this + return processedString +} + +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 +} + +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/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt new file mode 100644 index 00000000..5f71a974 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt @@ -0,0 +1,72 @@ +package org.smartregister.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.model.Base +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.EpisodeOfCare +import org.hl7.fhir.r4.model.Group +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.PlanDefinition +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 + +class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorkerContext) : + ITransformerServices { + + val outputs: MutableList = mutableListOf() + + override fun log(message: String) { + System.out.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) + } + } + + 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 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") + } +} diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt new file mode 100644 index 00000000..518a0adc --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt @@ -0,0 +1,527 @@ +package org.smartregister.structuremaptool + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.FhirVersionEnum +import org.apache.poi.ss.usermodel.Row +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext +import org.hl7.fhir.r4.model.Enumeration +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent +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 + +import org.smartregister.external.FhirPathEngineHostServices + +// Get the hl7 resources +val contextR4 = FhirContext.forR4() +val fhirResources = contextR4.resourceTypes +fun getQuestionsPath(questionnaire: Questionnaire): HashMap { + val questionsMap = hashMapOf() + + 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) + } +} + + +class Group( + 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() + } + + 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'" + } + } + + // 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 "''" + } + + + 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 += "." + } + } + + fieldPath = newFieldPath + } + + it.add(nextInstruction) + + return@run + } + } + + // 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) + } 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) + + 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) + } + + //nest!!.buildStructureMap(currLevel + 1) + + 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 +) { + 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" + } + } +} + +fun StringBuilder.appendNewLine(count: Int = 1): StringBuilder { + var counter = 0 + while (counter < count) { + append(System.lineSeparator()) + counter++ + } + return this +} + + +private val Field.isList: Boolean + get() = isParameterized && type == List::class.java + +private val Field.isParameterized: Boolean + 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 + +private fun Class<*>.getFieldOrNull(name: String): Field? { + 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 + } +} + +private fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + + return answer.firstOrNull()?.javaClass?.name +} + + +internal val fhirPathEngine: FHIRPathEngine = + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + // TODO update to link to already existing FHIRPathEngineHostService in the external folder + // hostServices = FHIRPathEngineHostServices + } + } + +private fun String.isEnumeration(instruction: Instruction): Boolean { + 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"; + } +} + +// 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 + } +} + +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.", "") +} + +fun String.isMultipleTypes(): Boolean = this == "Type" + +// TODO: Finish this. Use the annotation @Chid.type +fun String.getPossibleTypes(): List { + 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" + ) + + possibleConversions.forEach { + if (this.contains(it.first) && sourceType == it.second) { + return true + } + } + + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } + + return true +} + +fun String.getParentResource(): String? { + return substring(0, lastIndexOf('.')) +} + + +fun String.getResourceProperty(): String? { + return substring(lastIndexOf('.') + 1) +} + +fun String.getFhirType(): String = replace("Type", "") + .lowercase() \ No newline at end of file From 175e932b776357c7a4cd36abb7aa82bfabccccb1 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 11 Sep 2024 17:25:43 +0300 Subject: [PATCH 02/10] Initial commit to add sm generation tool to efsity Signed-off-by: Lentumunai-Mark --- efsity/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/efsity/.gitignore b/efsity/.gitignore index 635edfdf..067b0a01 100644 --- a/efsity/.gitignore +++ b/efsity/.gitignore @@ -9,3 +9,6 @@ fhircore-tooling.iml # build files bin target + +generated-structure-map.txt +generated-json-map.json \ No newline at end of file From 5b4752676cf81d47577f0b42084dcb5477989c47 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 11 Sep 2024 17:26:31 +0300 Subject: [PATCH 03/10] Integrate smGen into efsity initial start. Signed-off-by: Lentumunai-Mark --- efsity/.gitignore | 3 +- .../command/GenerateStructureMapCommand.java | 3 + .../external/TransformSupportServices.kt | 1 + .../GenerateStructureMapService.kt | 1 + .../TransformSupportServices.kt | 72 ------------------ .../FhirPathEngineHostServices.kt | 2 +- .../Main.kt | 4 +- .../Utils.kt | 21 ++--- ...{StructureMap XLS.xls => StructureMap.xls} | Bin .../FhirPathEngineHostServicesTest.kt | 4 +- 10 files changed, 22 insertions(+), 89 deletions(-) delete mode 100644 efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt rename sm-gen/src/main/resources/{StructureMap XLS.xls => StructureMap.xls} (100%) diff --git a/efsity/.gitignore b/efsity/.gitignore index 067b0a01..79acd051 100644 --- a/efsity/.gitignore +++ b/efsity/.gitignore @@ -9,6 +9,5 @@ fhircore-tooling.iml # build files bin target - generated-structure-map.txt -generated-json-map.json \ No newline at end of file +generated-json-map.json diff --git a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java index a3c15b0c..cd9d0e61 100644 --- a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java +++ b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java @@ -8,6 +8,9 @@ @CommandLine.Command(name = "generateStructureMap") public class GenerateStructureMapCommand implements Runnable { + + + //generateStructureMap --questionnaire /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire.json --configPath /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/StructureMap XLS.xls --questionnaireResponsePath /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire-response.json @CommandLine.Option( names = {"-q", "--questionnaire"}, description = "Questionnaire", diff --git a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt index bec23f46..7aa3b25d 100644 --- a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt +++ b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt @@ -40,6 +40,7 @@ class TransformSupportServices constructor(private val simpleWorkerContext: Simp 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() diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt index 640449d8..c9ba7b8a 100644 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt @@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion +import org.smartregister.external.TransformSupportServices import java.io.File import java.io.FileInputStream import java.nio.charset.Charset diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt deleted file mode 100644 index 5f71a974..00000000 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/TransformSupportServices.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.smartregister.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.model.Base -import org.hl7.fhir.r4.model.CarePlan -import org.hl7.fhir.r4.model.Coding -import org.hl7.fhir.r4.model.Encounter -import org.hl7.fhir.r4.model.EpisodeOfCare -import org.hl7.fhir.r4.model.Group -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.PlanDefinition -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 - -class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorkerContext) : - ITransformerServices { - - val outputs: MutableList = mutableListOf() - - override fun log(message: String) { - System.out.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) - } - } - - 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 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") - } -} 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 fd4f5c15..9d970769 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 @@ -51,7 +51,7 @@ internal object FhirPathEngineHostServices : FHIRPathEngine.IEvaluationContext { } override fun resolveFunction( - functionName: String? + functionName: String?, ): FHIRPathEngine.IEvaluationContext.FunctionDetails { logger.info("Resolving function: ${functionName ?: "Unknown"}") return functionCache.getOrPut(functionName ?: "") { 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 00e7f137..4b7c68a2 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 @@ -60,7 +60,7 @@ class Application : CliktCommand() { val questionnaire: Questionnaire = fhirJsonParser.parseResource( Questionnaire::class.java, - FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) + FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()), ) val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) @@ -68,7 +68,7 @@ class Application : CliktCommand() { questionnaireResponse = fhirJsonParser.parseResource( QuestionnaireResponse::class.java, - questionnaireResponseFile.readText(Charset.defaultCharset()) + questionnaireResponseFile.readText(Charset.defaultCharset()), ) } else { println("File not found: questionnaire-response.json") 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 843944d1..95e8630b 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 @@ -28,7 +28,7 @@ fun getQuestionsPath(questionnaire: Questionnaire): HashMap { fun getQuestionNames( parentName: String, item: QuestionnaireItemComponent, - questionsMap: HashMap + questionsMap: HashMap, ) { val currParentName = if (parentName.isEmpty()) "" else parentName questionsMap.put(item.linkId, currParentName) @@ -37,7 +37,7 @@ fun getQuestionNames( getQuestionNames( currParentName + ".where(linkId = '${item.linkId}').item", itemComponent, - questionsMap + questionsMap, ) } } @@ -58,7 +58,7 @@ class Group( sb.append("create('Reference') as reference then {") sb.appendNewLine() sb.append( - "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))" + "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))", ) sb.append(""" "rule_d";""".trimMargin()) sb.appendNewLine() @@ -77,7 +77,7 @@ class Group( stringBuilder.append(structureMapFunctionHead).appendNewLine() stringBuilder .append( - "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {" + "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {", ) .appendNewLine() @@ -293,11 +293,11 @@ class Group( answerExpression.startsWith("evaluate") ) { println( - "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType" + "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 = " + "create('${propertyType.getFhirType()}') as randomVal, randomVal.value = ", ) stringBuilder.append(answerExpression) addRuleNo() @@ -317,7 +317,7 @@ class Group( if (!name.equals("")) { val resourceType = resourceName stringBuilder.append( - "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {" + "src -> entity$currLevel.$name = create('$resourceType') as entity${currLevel + 1} then {", ) stringBuilder.appendNewLine() } else { @@ -435,7 +435,7 @@ private fun String.getType(questionnaireResponse: QuestionnaireResponse): String internal val fhirPathEngine: FHIRPathEngine = with(FhirContext.forCached(FhirVersionEnum.R4)) { FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { - hostServices = FHIRPathEngineHostServices + hostServices = FhirPathEngineHostServices } } @@ -509,8 +509,9 @@ fun String.getPossibleTypes(): List { 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") + if (sourceType == "StringType") { + String::class.java + } else Class.forName("org.hl7.fhir.r4.model.$sourceType") val possibleConversions = listOf( diff --git a/sm-gen/src/main/resources/StructureMap XLS.xls b/sm-gen/src/main/resources/StructureMap.xls similarity index 100% rename from sm-gen/src/main/resources/StructureMap XLS.xls rename to sm-gen/src/main/resources/StructureMap.xls 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 c1146e87..1f40c23a 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 @@ -34,7 +34,7 @@ class FhirPathEngineHostServicesTest { assertEquals( "Test Value", (result as StringType).value, - "The resolved constant should match the expected value" + "The resolved constant should match the expected value", ) } @@ -88,7 +88,7 @@ class FhirPathEngineHostServicesTest { null, mutableListOf(), "testFunction", - mutableListOf() + mutableListOf(), ) } assertEquals("executeFunction is not yet implemented.", exception.message) From 94e1cf56ac05d77ade149970346095e4e68c1fac Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 11 Sep 2024 17:37:36 +0300 Subject: [PATCH 04/10] Add missing dependencies and run spotless. Signed-off-by: Lentumunai-Mark --- efsity/build.gradle.kts | 2 - .../command/GenerateStructureMapCommand.java | 79 +- .../GenerateStructureMapService.kt | 427 ++++----- .../smartregister/structuremaptool/Utils.kt | 809 +++++++++--------- 4 files changed, 673 insertions(+), 644 deletions(-) diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index 5a266ce0..a1f5bb1a 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -86,8 +86,6 @@ dependencies { implementation(deps.javafaker) implementation("org.apache.poi:poi:4.1.1") implementation("org.apache.poi:poi-ooxml:4.1.1") - - testImplementation(kotlin("test")) testImplementation("junit:junit:4.13.2") diff --git a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java index cd9d0e61..53cf8063 100644 --- a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java +++ b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java @@ -1,51 +1,54 @@ package org.smartregister.command; +import java.io.IOException; import org.smartregister.structuremaptool.GenerateStructureMapServiceKt; import org.smartregister.util.FctUtils; import picocli.CommandLine; -import java.io.IOException; - @CommandLine.Command(name = "generateStructureMap") public class GenerateStructureMapCommand implements Runnable { - - //generateStructureMap --questionnaire /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire.json --configPath /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/StructureMap XLS.xls --questionnaireResponsePath /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire-response.json - @CommandLine.Option( - names = {"-q", "--questionnaire"}, - description = "Questionnaire", - required = true) - private String questionnairePath; - - @CommandLine.Option( - names = {"-c", "--configPath"}, - description = "StructureMap generation configuration in an excel sheet", - required = true) - private String configPath; - - @CommandLine.Option( - names = {"-qr", "--questionnaireResponsePath"}, - description = "Questionnaire response", - required = true) - private String questionnaireResponsePath; - - @Override - public void run(){ - if (configPath != null){ - try { - generateStructureMap(configPath, questionnairePath, questionnaireResponsePath); - } catch (IOException e){ - throw new RuntimeException(e); - } - } + // generateStructureMap --questionnaire + // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire.json --configPath + // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/StructureMap XLS.xls + // --questionnaireResponsePath + // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire-response.json + @CommandLine.Option( + names = {"-q", "--questionnaire"}, + description = "Questionnaire", + required = true) + private String questionnairePath; + + @CommandLine.Option( + names = {"-c", "--configPath"}, + description = "StructureMap generation configuration in an excel sheet", + required = true) + private String configPath; + + @CommandLine.Option( + names = {"-qr", "--questionnaireResponsePath"}, + description = "Questionnaire response", + required = true) + private String questionnaireResponsePath; + + @Override + public void run() { + if (configPath != null) { + try { + generateStructureMap(configPath, questionnairePath, questionnaireResponsePath); + } catch (IOException e) { + throw new RuntimeException(e); + } } + } - public static void generateStructureMap( - String configPath, String questionnairePath, String questionnaireResponsePath) throws IOException{ - long start = System.currentTimeMillis(); - FctUtils.printInfo("Starting StructureMap generation"); - GenerateStructureMapServiceKt.main(configPath, questionnairePath, questionnaireResponsePath); + public static void generateStructureMap( + String configPath, String questionnairePath, String questionnaireResponsePath) + throws IOException { + long start = System.currentTimeMillis(); + FctUtils.printInfo("Starting StructureMap generation"); + GenerateStructureMapServiceKt.main(configPath, questionnairePath, questionnaireResponsePath); - FctUtils.printCompletedInDuration(start); - } + FctUtils.printCompletedInDuration(start); + } } diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt index c9ba7b8a..d86f0b19 100644 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt @@ -3,6 +3,10 @@ package org.smartregister.structuremaptool import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser +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 @@ -15,261 +19,270 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion import org.smartregister.external.TransformSupportServices -import java.io.File -import java.io.FileInputStream -import java.nio.charset.Charset -import java.util.* fun main( - xlsConfigFilePath: String, - questionnaireFilePath: String, - questionnaireResponsePath: String + xlsConfigFilePath: String, + questionnaireFilePath: String, + questionnaireResponsePath: String ) { - val contextR4 = FhirContext.forR4() - val fhirJsonParser = contextR4.newJsonParser() - val questionnaire : Questionnaire = fhirJsonParser.parseResource( - Questionnaire::class.java, FileUtils.readFileToString(File(questionnaireFilePath), Charset.defaultCharset())) - val questionnaireResponse: QuestionnaireResponse = fhirJsonParser.parseResource( - QuestionnaireResponse::class.java, FileUtils.readFileToString(File(questionnaireResponsePath), Charset.defaultCharset())) - - // reads the xls - val xlsConfigFile = FileInputStream(xlsConfigFilePath) - val xlWorkbook = WorkbookFactory.create(xlsConfigFile) - - // 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 = xlWorkbook.getSheet("Settings") - var questionnaireId : String? = null - - 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 - } + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire: Questionnaire = + fhirJsonParser.parseResource( + Questionnaire::class.java, + FileUtils.readFileToString(File(questionnaireFilePath), Charset.defaultCharset()) + ) + val questionnaireResponse: QuestionnaireResponse = + fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, + FileUtils.readFileToString(File(questionnaireResponsePath), Charset.defaultCharset()) + ) + + // reads the xls + val xlsConfigFile = FileInputStream(xlsConfigFilePath) + val xlWorkbook = WorkbookFactory.create(xlsConfigFile) + + // 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 = xlWorkbook.getSheet("Settings") + var questionnaireId: String? = null + + 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 } + } - /* - 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!!!! - */ + /* + 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 questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } + if (questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()) { - val sb = StringBuilder() - val structureMapHeader = """ + 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() - - val resourceConversionInstructions = hashMapOf>() - - // Group the rules according to the resource - val fieldMappingsSheet = xlWorkbook.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 + src -> bundle.entry as entry then """ + .trimIndent() + + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWorkbook.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) + // Perform the extraction for the row + /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) - extractionResources[resourceName + resourceIndex] = resource*/ + extractionResources[resourceName + resourceIndex] = resource*/ - sb.append(structureMapHeader) - sb.appendNewLine(2) - sb.append(structureMapBody) + sb.append(structureMapHeader) + sb.appendNewLine(2) + sb.append(structureMapBody) - // Fix the question path - val questionsPath = getQuestionsPath(questionnaire) + // Fix the question 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)") + // 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() } - sb.append(""" "rule_a";""".trimMargin()) - sb.appendNewLine() - sb.append("}") + 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(3) + // Add the embedded instructions + val groupNames = mutableListOf() + sb.appendNewLine(3) - resourceConversionInstructions.forEach { - Group(it, sb, questionsPath) - .generateGroup(questionnaireResponse) - } + 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() + val structureMapString = sb.toString() + try { + val simpleWorkerContext = + SimpleWorkerContext().apply { + setExpansionProfile(Parameters()) + isCanRunWithoutTerminology = true } - - var finalStructureMap = sb.toString() - finalStructureMap = finalStructureMap.addIndentation() - println(finalStructureMap) - writeStructureMapOutput(sb.toString().addIndentation()) - + 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 String.clean() : String { - return this.replace("-", "") - .replace("_", "") - .replace(" ", "") +fun String.clean(): String { + return this.replace("-", "").replace("_", "").replace(" ", "") } -fun Row.isEmpty() : Boolean { - return getCell(0) == null && getCell(1) == null && getCell(2) == null +fun Row.isEmpty(): Boolean { + return getCell(0) == null && getCell(1) == null && getCell(2) == null } -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.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.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 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 + } } 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 - - // TODO: Clean the following properties - var fullFieldPath = "" - fun fullPropertyPath() : String = "$resource.$fullFieldPath" - - fun searchKey() = resource + resourceIndex -} + 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 -fun String.addIndentation() : String { - var currLevel = 0 - val lines = split("\n") + // TODO: Clean the following properties + var fullFieldPath = "" - 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() + fun fullPropertyPath(): String = "$resource.$fullFieldPath" + + fun searchKey() = resource + resourceIndex } -fun String.addIndentation(times: Int) : String { - var processedString = "" - for (k in 1..times) { - processedString += "\t" +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() +} + +fun String.addIndentation(times: Int): String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } - processedString += this - return processedString + processedString += this + return processedString } 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 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) +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/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt index 518a0adc..64f6ab53 100644 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt @@ -2,6 +2,8 @@ package org.smartregister.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,517 +13,530 @@ 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 - -import org.smartregister.external.FhirPathEngineHostServices // 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 + } - 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 + if (index > 0 && index < parts.size - 1) { + newFieldPath += "." + } + } - 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(count: Int = 1): StringBuilder { - var counter = 0 - while (counter < count) { - append(System.lineSeparator()) - counter++ - } - return this + var counter = 0 + while (counter < count) { + append(System.lineSeparator()) + counter++ + } + 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 { - // TODO update to link to already existing FHIRPathEngineHostService in the external folder - // hostServices = FHIRPathEngineHostServices - } + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + // TODO update to link to already existing FHIRPathEngineHostService in the external folder + // 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 resourcePropertyName = parts[index] + val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) - val propertyType = if (propertyField?.isList == true) - propertyField.nonParameterizedType + 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.", "") + 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() From c6038481cec8386ff4a8ab46251e9eb58ee67d8c Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Mon, 16 Sep 2024 10:24:46 +0300 Subject: [PATCH 05/10] Implement string extensions. Signed-off-by: Lentumunai-Mark --- efsity/build.gradle.kts | 1 + .../external/TransformSupportServices.kt | 2 +- .../GenerateStructureMapService.kt | 38 +---- .../smartregister/structuremaptool/Utils.kt | 82 +---------- .../extensions/StringExtensions.kt | 121 ++++++++++++++++ .../external/TransformSupportServicesTest.kt | 137 ++++++++++++++++++ 6 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt create mode 100644 efsity/src/test/kotlin/external/TransformSupportServicesTest.kt diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index a1f5bb1a..ea0fe44e 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -90,6 +90,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-inline:3.12.4") + testImplementation("io.mockk:mockk:1.13.7") } tasks.withType { options.encoding = deps.versions.project.build.sourceEncoding.get() } diff --git a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt index 7aa3b25d..3afc274c 100644 --- a/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt +++ b/efsity/src/main/kotlin/org/smartregister/external/TransformSupportServices.kt @@ -30,7 +30,7 @@ import org.hl7.fhir.r4.utils.StructureMapUtilities.ITransformerServices class TransformSupportServices constructor(private val simpleWorkerContext: SimpleWorkerContext) : ITransformerServices { - private val outputs: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() override fun log(message: String) { // logger.info(message) diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt index d86f0b19..340cf1e9 100644 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/GenerateStructureMapService.kt @@ -19,6 +19,8 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion import org.smartregister.external.TransformSupportServices +import org.smartregister.structuremaptool.extensions.addIndentation +import org.smartregister.structuremaptool.extensions.clean fun main( xlsConfigFilePath: String, @@ -178,10 +180,6 @@ fun main( } } -fun String.clean(): String { - return this.replace("-", "").replace("_", "").replace(" ", "") -} - fun Row.isEmpty(): Boolean { return getCell(0) == null && getCell(1) == null && getCell(2) == null } @@ -229,38 +227,6 @@ class Instruction { fun searchKey() = resource + resourceIndex } -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() -} - -fun String.addIndentation(times: Int): String { - var processedString = "" - for (k in 1..times) { - processedString += "\t" - } - - processedString += this - return processedString -} - fun Instruction.copyFrom(instruction: Instruction) { constantValue = instruction.constantValue resource = instruction.resource diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt index 64f6ab53..18cdf6b0 100644 --- a/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/Utils.kt @@ -11,8 +11,8 @@ import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent 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 org.smartregister.structuremaptool.extensions.* // Get the hl7 resources val contextR4 = FhirContext.forR4() @@ -417,21 +417,6 @@ private fun Class<*>.getFieldOrNull(name: String): Field? { } } -private fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { - 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) - - return answer.firstOrNull()?.javaClass?.name -} - internal val fhirPathEngine: FHIRPathEngine = with(FhirContext.forCached(FhirVersionEnum.R4)) { FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { @@ -440,25 +425,6 @@ internal val fhirPathEngine: FHIRPathEngine = } } -private fun String.isEnumeration(instruction: Instruction): Boolean { - 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" - } -} - -// 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` @@ -494,49 +460,3 @@ fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? 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() -} - -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" - ) - - possibleConversions.forEach { - if (this.contains(it.first) && sourceType == it.second) { - return true - } - } - - try { - propertyClass.getDeclaredMethod("fromCode", targetType2) - } catch (ex: NoSuchMethodException) { - return false - } - - return true -} - -fun String.getParentResource(): String? { - return substring(0, lastIndexOf('.')) -} - -fun String.getResourceProperty(): String? { - return substring(lastIndexOf('.') + 1) -} - -fun String.getFhirType(): String = replace("Type", "").lowercase() diff --git a/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt b/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt new file mode 100644 index 00000000..2f56bc24 --- /dev/null +++ b/efsity/src/main/kotlin/org/smartregister/structuremaptool/extensions/StringExtensions.kt @@ -0,0 +1,121 @@ +package org.smartregister.structuremaptool.extensions + +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Type +import org.smartregister.structuremaptool.* +import org.smartregister.structuremaptool.fhirPathEngine + +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() +} + +fun String.addIndentation(times: Int): String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } + + processedString += this + return processedString +} + +fun String.clean(): String { + return this.replace("-", "").replace("_", "").replace(" ", "") +} + +fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { + val answerType = getType(questionnaireResponse) + return if (answerType != null) { + answerType == "org.hl7.fhir.r4.model.Coding" + } else { + false + } +} + +fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + + return answer.firstOrNull()?.javaClass?.name +} + +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" + } +} + +fun String.isEnumeration(instruction: Instruction): Boolean { + return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false +} + +fun String.isEvaluateExpression(): Boolean = startsWith("evaluate(") + +fun String.isMultipleTypes(): Boolean = this == "Type" + +// TODO: Finish this. Use the annotation @Chid.type +fun String.getPossibleTypes(): List { + 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" + ) + + possibleConversions.forEach { + if (this.contains(it.first) && sourceType == it.second) { + return true + } + } + + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } + + return true +} + +fun String.getParentResource(): String? { + return substring(0, lastIndexOf('.')) +} + +fun String.getResourceProperty(): String? { + return substring(lastIndexOf('.') + 1) +} + +fun String.getFhirType(): String = replace("Type", "").lowercase() diff --git a/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt b/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt new file mode 100644 index 00000000..714ff886 --- /dev/null +++ b/efsity/src/test/kotlin/external/TransformSupportServicesTest.kt @@ -0,0 +1,137 @@ +package org.smartregister.fhir.structuremaptool + +import io.mockk.mockk +import kotlin.test.Test +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 org.smartregister.external.TransformSupportServices + +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") + } + } +} From fc5bcedbe6a2f9df3407c947ead2f6865fd5d7d1 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Tue, 17 Sep 2024 09:15:55 +0300 Subject: [PATCH 06/10] Add Fhir path engine host services tests. Signed-off-by: Lentumunai-Mark --- .../FhirPathEngineHostServicesTest.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt diff --git a/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt b/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt new file mode 100644 index 00000000..4f3a1062 --- /dev/null +++ b/efsity/src/test/kotlin/external/FhirPathEngineHostServicesTest.kt @@ -0,0 +1,96 @@ +package external + +import org.hl7.fhir.r4.model.StringType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.smartregister.external.FhirPathEngineHostServices + +class FhirPathEngineHostServicesTest { + + @Test + fun `test Resolve Constant with Valid Key returns Value`() { + val appContext = mapOf("test" to mutableListOf(StringType("Test Value"))) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) + + assertNotNull(result) + assertTrue(result is MutableList<*>) + assertEquals("Test Value", (result!![0] as StringType).value) + } + + @Test + fun `test Resolve Constant with Invalid Key returns Null`() { + val appContext = mapOf("test" to mutableListOf(StringType("Test Value"))) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "invalidKey", false) + + assertNull(result) + } + + @Test + fun `test Resolve Constant with NonMapAppContext returns Null`() { + val appContext = "invalidAppContext" + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) + + assertNull(result) + } + + @Test + fun `test Resolve Constant Type throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveConstantType(null, "test") + } + } + + @Test + fun `test Log throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.log("Test log message", mutableListOf()) + } + } + + @Test + fun `test Resolve Function throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveFunction("testFunction") + } + } + + @Test + fun `test Check Function throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) + } + } + + @Test + fun `test ExecuteFunction throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.executeFunction( + null, + mutableListOf(), + "testFunction", + mutableListOf() + ) + } + } + + @Test + fun `test Resolve Reference throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveReference(null, "http://example.com", null) + } + } + + @Test + fun `test Conforms To Profile throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.conformsToProfile(null, null, "http://example.com") + } + } + + @Test + fun `test Resolve ValueSet throws Unsupported Operation Exception`() { + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveValueSet(null, "http://example.com") + } + } +} From 188834bb7b5474e428e0e22a37f3f27553f4ad91 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Tue, 17 Sep 2024 10:00:22 +0300 Subject: [PATCH 07/10] Update fct documentation to include SM generation. Signed-off-by: Lentumunai-Mark --- efsity/README.md | 13 +++++++++++++ .../command/GenerateStructureMapCommand.java | 6 ------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/efsity/README.md b/efsity/README.md index 32fb06e7..2328235e 100644 --- a/efsity/README.md +++ b/efsity/README.md @@ -172,6 +172,19 @@ $ fct validateFileStructure -i ~/Workspace/fhir-resources/ -s ~/path/to If the file structure matches the schema then a positive result is printed to the terminal, otherwise an error is thrown showing the issue. +### Generating a structure map +To generate a Structure Map, you need to provide the questionnaire, the configuration path, and the questionnaire response path. The output will be based on the specified inputs +```console +$ $ generateStructureMap --questionnaire /path/to/questionnaire.json --configPath /path/to/config/StructureMap.xls --questionnaireResponsePath /path/to/questionnaire-response.json +``` +This process will generate two output files: a .map file and its JSON equivalent +**Options** +``` +--questionnaire Path to the Questionnaire JSON file. +--configPath Path to the Structure Map configuration file (XLS format). +--questionnaireResponsePath Path to the Questionnaire Response JSON file. +``` + ### Localization Tool that supports localization by the use of the translation extension diff --git a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java index 53cf8063..de8a4359 100644 --- a/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java +++ b/efsity/src/main/java/org/smartregister/command/GenerateStructureMapCommand.java @@ -7,12 +7,6 @@ @CommandLine.Command(name = "generateStructureMap") public class GenerateStructureMapCommand implements Runnable { - - // generateStructureMap --questionnaire - // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire.json --configPath - // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/StructureMap XLS.xls - // --questionnaireResponsePath - // /Users/markloshilu/Ona/fhir-tooling/sm-gen/src/main/resources/questionnaire-response.json @CommandLine.Option( names = {"-q", "--questionnaire"}, description = "Questionnaire", From 9e6cf779a1dc89b9bcf86e0430a16163be10fbbc Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Wed, 18 Sep 2024 15:22:24 +0300 Subject: [PATCH 08/10] update Documentation for generating structure map. Signed-off-by: Lentumunai-Mark --- efsity/README.md | 8 ++++---- efsity/build.gradle.kts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/efsity/README.md b/efsity/README.md index 2328235e..8a29abb9 100644 --- a/efsity/README.md +++ b/efsity/README.md @@ -175,14 +175,14 @@ is thrown showing the issue. ### Generating a structure map To generate a Structure Map, you need to provide the questionnaire, the configuration path, and the questionnaire response path. The output will be based on the specified inputs ```console -$ $ generateStructureMap --questionnaire /path/to/questionnaire.json --configPath /path/to/config/StructureMap.xls --questionnaireResponsePath /path/to/questionnaire-response.json +$ fct generateStructureMap --questionnaire /path/to/questionnaire.json --configPath /path/to/config/StructureMap.xls --questionnaireResponsePath /path/to/questionnaire-response.json ``` This process will generate two output files: a .map file and its JSON equivalent **Options** ``` ---questionnaire Path to the Questionnaire JSON file. ---configPath Path to the Structure Map configuration file (XLS format). ---questionnaireResponsePath Path to the Questionnaire Response JSON file. +-q or --questionnaire Path to the Questionnaire JSON file. +-c or --configPath Path to the Structure Map configuration file (XLS format). +-qr or --questionnaireResponsePath Path to the Questionnaire Response JSON file. ``` ### Localization diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index ea0fe44e..bdb64ad2 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -91,6 +91,7 @@ dependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-inline:3.12.4") testImplementation("io.mockk:mockk:1.13.7") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") } tasks.withType { options.encoding = deps.versions.project.build.sourceEncoding.get() } From be963cd3fc27cb03cb4e6e1eb0ca9f1ca3ca0b60 Mon Sep 17 00:00:00 2001 From: Lentumunai-Mark Date: Mon, 14 Oct 2024 09:38:38 +0300 Subject: [PATCH 09/10] ntation. Signed-off-by: Lentumunai-Mark --- efsity/README.md | 17 +++++++++++++++++ efsity/Structure_map_gen.md | 37 +++++++++++++++++++++++++++++++++++++ efsity/build.gradle.kts | 4 ++-- efsity/libs.versions.toml | 4 ++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 efsity/Structure_map_gen.md diff --git a/efsity/README.md b/efsity/README.md index 8a29abb9..964e5704 100644 --- a/efsity/README.md +++ b/efsity/README.md @@ -184,6 +184,23 @@ This process will generate two output files: a .map file and its JSON equivalent -c or --configPath Path to the Structure Map configuration file (XLS format). -qr or --questionnaireResponsePath Path to the Questionnaire Response JSON file. ``` +**Sample Configuration File** + +To help users get started with the Structure Map generation process, we provide a sample configuration file in XLS format. You can download the sample from the link below: + +[Download Sample Structure Map Configuration (XLS)](https://github.com/onaio/fhir-tooling/blob/main/sm-gen/src/main/resources/StructureMap%20XLS.xls) + +Alternatively, if you have cloned the repository, the sample file can be found in the following path: + +console +``` +/path/to/your/repository/src/main/resources/StructureMapXLS.xls +``` + +This sample XLS form includes the required structure and format to define mappings between questionnaire fields and FHIR resources. You can customize it according to your specific needs. + +For more documentation please refer to this [documentation](https://github.com/onaio/fhir-tooling/blob/main/efsity/Structure_map_gen.md) + ### Localization Tool that supports localization by the use of the translation extension diff --git a/efsity/Structure_map_gen.md b/efsity/Structure_map_gen.md new file mode 100644 index 00000000..e2ba4836 --- /dev/null +++ b/efsity/Structure_map_gen.md @@ -0,0 +1,37 @@ +# StructureMap tooling + +The StructureMap Tooling is a utility designed to generate Structure Maps for FHIR (Fast Healthcare Interoperability Resources) transformations. Structure Maps are used to define how one FHIR resource is transformed into another. This tooling leverages the HL7 FHIRPath language to provide support for creating and managing these transformations. + +## Features +**FHIRPath Engine**: Utilizes the FHIRPath engine to evaluate and transform FHIR resources. + +**Transformation Support**: Provides services to support the transformation process, ensuring that resources are accurately converted according to the specified Structure Map. + +**Automation**: Simplifies the process of generating Structure Maps through automated tooling. + +## Files +**FhirPathEngineHostServices.kt**: Contains services for hosting and running the FHIRPath engine. + +**Main.kt**: The entry point of the application. It orchestrates the process of reading input, processing it through the FHIRPath engine, and generating the Structure Map. + +**TransformSupportServices.kt**: Provides additional support services required for the transformation process. + +**Utils.kt**: Contains the main logic for generating the Structure Maps using the FHIRPath engine and transformation support services. + +## Prerequisites +- Questionnaire JSON +- XLS form with the required information based on the questionnaire +### Installation +1. Clone the Repository: + +```console +git clone https://github.com/your-repo/structuremap-tooling.git +cd structuremap-tooling +``` +2. Once the `structuremap-tooling` folder, click on run. +3. A prompt will appear on the CLI `Kindly enter the XLS filepath`: Enter the absolute path of the file's location. Click `Enter` +4. Another prompt will appear on the CLI `Kindly enter the questionnaire filepath`: Enter the absolute path of the file's location. Click `Enter` +5. The structureMap will be generated in the CLI and two complete files in the folder containing the `.json` and `.map` files + +## Contributing +Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes. \ No newline at end of file diff --git a/efsity/build.gradle.kts b/efsity/build.gradle.kts index bdb64ad2..5139e1d5 100644 --- a/efsity/build.gradle.kts +++ b/efsity/build.gradle.kts @@ -84,8 +84,8 @@ dependencies { implementation(deps.xstream) implementation(deps.icu4j) implementation(deps.javafaker) - implementation("org.apache.poi:poi:4.1.1") - implementation("org.apache.poi:poi-ooxml:4.1.1") + implementation(deps.poi) + implementation(deps.poiooxml) testImplementation(kotlin("test")) testImplementation("junit:junit:4.13.2") diff --git a/efsity/libs.versions.toml b/efsity/libs.versions.toml index 9dd17858..4964b7e3 100644 --- a/efsity/libs.versions.toml +++ b/efsity/libs.versions.toml @@ -18,6 +18,8 @@ project-build-sourceEncoding="UTF-8" spotless-version ="6.20.0" xstream="1.4.20" icu4j-version = "75.1" +poi-version = "4.1.1" +poi-ooxml-version = "4.1.1" [libraries] caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine-version" } @@ -48,6 +50,8 @@ javafaker = { module = "com.github.javafaker:javafaker", version.ref = "javafake picocli = { module = "info.picocli:picocli", version.ref = "info-picocli-version" } xstream = { module = "com.thoughtworks.xstream:xstream", version.ref = "xstream" } icu4j = { module="com.ibm.icu:icu4j", version.ref = "icu4j-version" } +poi = { module="org.apache.poi:poi", version.ref = "poi-version" } +poiooxml = { module="org.apache.poi:poi-ooxml", version.ref = "poi-ooxml-version" } [bundles] cqf-cql = ["cql-to-elm","elm","elm-jackson","model","model-jackson"] From 322fce5411a4341e30182a00f578bd4f4f70dce1 Mon Sep 17 00:00:00 2001 From: Peter Lubell-Doughtie Date: Fri, 18 Oct 2024 10:44:55 -0400 Subject: [PATCH 10/10] Update Structure_map_gen.md --- efsity/Structure_map_gen.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/efsity/Structure_map_gen.md b/efsity/Structure_map_gen.md index e2ba4836..7204b2b9 100644 --- a/efsity/Structure_map_gen.md +++ b/efsity/Structure_map_gen.md @@ -21,6 +21,7 @@ The StructureMap Tooling is a utility designed to generate Structure Maps for FH ## Prerequisites - Questionnaire JSON - XLS form with the required information based on the questionnaire + ### Installation 1. Clone the Repository: @@ -34,4 +35,4 @@ cd structuremap-tooling 5. The structureMap will be generated in the CLI and two complete files in the folder containing the `.json` and `.map` files ## Contributing -Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes. \ No newline at end of file +Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.