From f6b2393124bbdf91685e31e5c6c67f9ef6c7df02 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Fri, 23 Aug 2024 12:17:28 +0300 Subject: [PATCH 1/9] Enhance determineFhirDataType function --- .../Main.kt | 62 ++++++ .../Utils.kt | 193 ++++++++++++++---- 2 files changed, 217 insertions(+), 38 deletions(-) diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt index 272e90b2..b200248f 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 @@ -9,6 +9,7 @@ import com.github.ajalt.clikt.parameters.options.prompt 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.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Resource @@ -70,7 +71,11 @@ class Application : CliktCommand() { val xlsFile = FileInputStream(xlsfile) val xlWb = WorkbookFactory.create(xlsFile) + // Validate resources and paths in the XLS sheet + validateResourcesAndPaths(xlWb) + // Fix groups calling sequence + fixGroupCallingSequence(xlWb) // 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 @@ -221,6 +226,63 @@ class Application : CliktCommand() { } } + private fun validateResourcesAndPaths(workbook: Workbook) { + val fieldMappingsSheet = workbook.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + + val resourceName = row.getCellAsString(2) + val fieldPath = row.getCellAsString(4) + + if (!isValidResource(resourceName)) { + throw IllegalArgumentException("Invalid resource name: $resourceName") + } + + if (!isValidPath(fieldPath)) { + throw IllegalArgumentException("Invalid field path: $fieldPath") + } + } + } + private fun isValidResource(resourceName: String?): Boolean { + // Implement logic to validate resource names + // This can be a list of known valid resource names, or a more complex validation + return resourceName != null && resourceName.isNotEmpty() + } + + private fun isValidPath(path: String?): Boolean { + // Implement logic to validate paths + // This can involve checking against known paths or ensuring the format is correct + return path != null && path.isNotEmpty() + } + + private fun fixGroupCallingSequence(workbook: Workbook) { + // Implement logic to fix group calling sequences + // Detect and handle cyclic dependencies, using topological sorting or other methods + // You can throw an exception if a cyclic dependency is detected + } + + private fun groupRulesByResource(workbook: Workbook, questionnaireResponseItemIds: List): Map> { + val fieldMappingsSheet = workbook.getSheet("Field Mappings") + val resourceConversionInstructions = hashMapOf>() + + 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) + } + } + + return resourceConversionInstructions + } fun Row.getInstruction() : Instruction { return Instruction().apply { diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt index cdda9b4f..d214a9e7 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 @@ -13,6 +13,7 @@ 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 kotlin.reflect.KClass // Get the hl7 resources val contextR4 = FhirContext.forR4() @@ -320,50 +321,133 @@ fun generateStructureMapLine( 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'") - } + val fieldPath = row.getCell(4)?.stringCellValue ?: "" + val cellValue = row.getCell(0)?.stringCellValue ?: "" - "integer" -> { - structureMapBody.append("create('integer').value = $cellValue") - } + // Determine the target FHIR data type + val targetDataType = determineFhirDataType(cellValue) - "boolean" -> { - val booleanValue = - if (cellValue.equals("true", ignoreCase = true)) "true" else "false" - structureMapBody.append("create('boolean').value = $booleanValue") - } + // Generate the mapping line for the StructureMap + structureMapBody.append("src -> entity.$fieldPath = ") - else -> { - structureMapBody.append("create('unsupportedDataType').value = '$cellValue'") - } + // Handle different data types + when (targetDataType) { + "string" -> { + structureMapBody.append("create('string').value = '${cellValue.escapeQuotes()}'") + } + + "integer" -> { + structureMapBody.append("create('integer').value = ${cellValue.toIntOrNull() ?: 0}") } - structureMapBody.appendNewLine() - } -} -fun determineFhirDataType(cellValue: String): String { - val cleanedValue = cellValue.trim().toLowerCase() + "boolean" -> { + val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" + structureMapBody.append("create('boolean').value = $booleanValue") + } + + "date" -> { + // Handle date type + structureMapBody.append("create('date').value = '${cellValue.escapeQuotes()}'") + } + + "decimal" -> { + // Handle decimal type + structureMapBody.append("create('decimal').value = ${cellValue.toDoubleOrNull() ?: 0.0}") + } + + "code" -> { + // Handle code type + structureMapBody.append("create('code').value = '${cellValue.escapeQuotes()}'") + } - 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" + structureMapBody.append("create('unsupportedDataType').value = '${cellValue.escapeQuotes()}'") } } + structureMapBody.appendNewLine() } +fun String.escapeQuotes(): String { + return this.replace("'", "\\'") +} + +fun determineFhirDataType(input: String?): String { + if (input.isNullOrEmpty()) { + return "Invalid Input: Null or Empty String" + } + + // Clean and prepare the input for matching + val cleanedValue = input.trim().toLowerCase() + + // Regular Expressions for FHIR Data Types + val booleanRegex = "^(true|false)\$".toRegex(RegexOption.IGNORE_CASE) + val integerRegex = "^-?\\d+\$".toRegex() + val decimalRegex = "^-?\\d*\\.\\d+\$".toRegex() + val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() // YYYY-MM-DD + val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() // YYYY-MM-DDThh:mm:ssZ + val stringRegex = "^[\\w\\s]+\$".toRegex() + val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() // e.g., "70 kg" + val codeableConceptRegex = "^[\\w\\s]+\$".toRegex() // Simplified for now + val codingRegex = "^\\w+\\|\$".toRegex() // Simplified for now + val referenceRegex = "^\\w+/\\w+\$".toRegex() // e.g., "Patient/123" + val periodRegex = "^\\d{4}-\\d{2}-\\d{2}\\/\\d{4}-\\d{2}-\\d{2}\$".toRegex() // e.g., "2023-01-01/2023-12-31" + val timingRegex = "^\\d+[a-zA-Z]+\$".toRegex() // Simplified for now + val rangeRegex = "^\\d+-\\d+\$".toRegex() // e.g., "10-20" + val annotationRegex = """^.*\s+\S+""".toRegex() // A basic regex for general text or comments + val attachmentRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 encoded string (could be more complex) + val base64BinaryRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Similar to attachment, but could have specific markers + val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // Regex for phone numbers (E.164 format) + val humanNameRegex = """^[A-Za-z\s'-]+$""".toRegex() // Simple regex for names + val addressRegex = """^\d+\s[A-Za-z]+\s[A-Za-z]+""".toRegex() // Basic address pattern + val durationRegex = """^\d+\s(hour|minute|second|day)$""".toRegex() // Duration like "1 hour" + val moneyRegex = """^\d+(\.\d{2})?\s[A-Z]{3}$""".toRegex() // Money format like "100.00 USD" + val ratioRegex = """^\d+:\d+$""".toRegex() // Simple ratio like "1:1000" + val signatureRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 signature + val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() // Identifier format + val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() // Simple URI format + val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() // UUID format + val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() // ISO 8601 instant format + val narrativeRegex = """.*<\/div>""".toRegex() // Narrative XHTML content + val sampledDataRegex = """^.*\s+\S+""".toRegex() // Placeholder regex for complex observation data + val backboneElementRegex = """^.*$""".toRegex() // Catch-all for complex structures (requires specific context) + // Detect and Return FHIR Data Type + return when { + booleanRegex.matches(cleanedValue) -> "Boolean" + integerRegex.matches(cleanedValue) -> "Integer" + decimalRegex.matches(cleanedValue) -> "Decimal" + dateRegex.matches(cleanedValue) -> "Date" + dateTimeRegex.matches(cleanedValue) -> "DateTime" + quantityRegex.matches(input) -> "Quantity" + codeableConceptRegex.matches(input) -> "CodeableConcept" + codingRegex.matches(input) -> "Coding" + identifierRegex.matches(input) -> "Identifier" + referenceRegex.matches(input) -> "Reference" + periodRegex.matches(input) -> "Period" + timingRegex.matches(input) -> "Timing" + rangeRegex.matches(input) -> "Range" + stringRegex.matches(input) -> "String" + annotationRegex.matches(input) -> "Annotation" + attachmentRegex.matches(input) -> "Attachment" + base64BinaryRegex.matches(input) -> "Base64Binary" + contactPointRegex.matches(input) -> "ContactPoint" + humanNameRegex.matches(input) -> "HumanName" + addressRegex.matches(input) -> "Address" + durationRegex.matches(input) -> "Duration" + moneyRegex.matches(input) -> "Money" + ratioRegex.matches(input) -> "Ratio" + signatureRegex.matches(input) -> "Signature" + identifierRegex.matches(input) -> "Identifier" + uriRegex.matches(input) -> "Uri" + uuidRegex.matches(input) -> "Uuid" + instantRegex.matches(input) -> "Instant" + narrativeRegex.matches(input) -> "Narrative" + sampledDataRegex.matches(input) -> "SampledData" + backboneElementRegex.matches(input) -> "BackboneElement" + else -> "String" + } +} + + fun StringBuilder.appendNewLine(): StringBuilder { append(System.lineSeparator()) return this @@ -475,22 +559,46 @@ fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? fun String.isMultipleTypes(): Boolean = this == "Type" -// TODO: Finish this. Use the annotation @Chid.type +// Assuming a mock annotation to simulate the @Child.type annotation in FHIR +annotation class Child(val type: KClass) fun String.getPossibleTypes(): List { - return listOf() + val clazz = Class.forName("org.hl7.fhir.r4.model.$this") + val possibleTypes = mutableListOf() + + clazz.declaredFields.forEach { field -> + val annotation = field.annotations.find { it is Child } as? Child + annotation?.let { + val typeInstance = it.type.java.getDeclaredConstructor().newInstance() + possibleTypes.add(typeInstance) + } + } + + return possibleTypes } 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 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" + "AdministrativeGender" to "CodeType", + "DateTimeType" to "StringType", + "TimeType" to "StringType", + "InstantType" to "DateTimeType", + "UriType" to "StringType", + "UuidType" to "StringType", + "CodeType" to "StringType", + "MarkdownType" to "StringType", + "Base64BinaryType" to "StringType", + "OidType" to "StringType", + "PositiveIntType" to "IntegerType", + "UnsignedIntType" to "IntegerType", + "IdType" to "StringType", + "CanonicalType" to "StringType" ) possibleConversions.forEach { @@ -499,6 +607,14 @@ fun String.canHandleConversion(sourceType: String): Boolean { } } + // Check if the source type can be converted to any of the possible types for this target type + val possibleTypes = this.getPossibleTypes() + possibleTypes.forEach { possibleType -> + if (possibleType::class.simpleName == sourceType) { + return true + } + } + try { propertyClass.getDeclaredMethod("fromCode", targetType2) } catch (ex: NoSuchMethodException) { @@ -508,6 +624,7 @@ fun String.canHandleConversion(sourceType: String): Boolean { return true } + fun String.getParentResource(): String? { return substring(0, lastIndexOf('.')) } From 178fe1d2db5c656a0ef2b611f0dacc9a066f2620 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Wed, 28 Aug 2024 17:09:43 +0300 Subject: [PATCH 2/9] Fix determineFhirDataType --- .../Main.kt | 23 ++-- .../Utils.kt | 128 +++++++----------- .../kotlin/utils/DetermineFhirDataTypeTest.kt | 96 +++++++++++++ 3 files changed, 155 insertions(+), 92 deletions(-) create mode 100644 sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt 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 b200248f..9b99c0ed 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 @@ -157,11 +157,11 @@ class Application : CliktCommand() { } //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*/ +// +// // Perform the extraction for the row +// /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) +// +// extractionResources[resourceName + resourceIndex] = resource*/ sb.append(structureMapHeader) sb.appendNewLine().appendNewLine().appendNewLine() @@ -175,22 +175,17 @@ class Application : CliktCommand() { var len = resourceConversionInstructions.size var resourceName = "" resourceConversionInstructions.forEach { entry -> - resourceName = entry.key.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + val resourceName = entry.key.replaceFirstChar { it.titlecase(Locale.getDefault()) } if (index++ != 0) sb.append(",") - if(resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") + sb.append("Extract$resourceName(src, bundle)") } sb.append(""" "rule_a";""".trimMargin()) - sb.appendNewLine() - sb.append("}") - - // Add the embedded instructions - val groupNames = mutableListOf() + sb.appendNewLine().append("}") sb.appendNewLine().appendNewLine().appendNewLine() resourceConversionInstructions.forEach { - Group(it, sb, questionsPath) - .generateGroup(questionnaireResponse) + Group(it, sb, questionsPath).generateGroup(questionnaireResponse) } val structureMapString = sb.toString() 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 d214a9e7..d814d420 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 @@ -335,38 +335,25 @@ fun generateStructureMapLine( "string" -> { structureMapBody.append("create('string').value = '${cellValue.escapeQuotes()}'") } - "integer" -> { structureMapBody.append("create('integer').value = ${cellValue.toIntOrNull() ?: 0}") } - "boolean" -> { - val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" - structureMapBody.append("create('boolean').value = $booleanValue") + structureMapBody.append("create('boolean').value = ${cellValue.toBoolean()}") } - "date" -> { - // Handle date type - structureMapBody.append("create('date').value = '${cellValue.escapeQuotes()}'") - } - - "decimal" -> { - // Handle decimal type - structureMapBody.append("create('decimal').value = ${cellValue.toDoubleOrNull() ?: 0.0}") - } - - "code" -> { - // Handle code type - structureMapBody.append("create('code').value = '${cellValue.escapeQuotes()}'") + structureMapBody.append("create('date').value = '${cellValue}'") } - + // Add more cases for other FHIR types as needed else -> { - structureMapBody.append("create('unsupportedDataType').value = '${cellValue.escapeQuotes()}'") + structureMapBody.append("create('$targetDataType').value = '${cellValue.escapeQuotes()}'") } } - structureMapBody.appendNewLine() + + structureMapBody.append(";") } + fun String.escapeQuotes(): String { return this.replace("'", "\\'") } @@ -376,73 +363,58 @@ fun determineFhirDataType(input: String?): String { return "Invalid Input: Null or Empty String" } - // Clean and prepare the input for matching - val cleanedValue = input.trim().toLowerCase() + val cleanedValue = input.trim() // Regular Expressions for FHIR Data Types val booleanRegex = "^(true|false)\$".toRegex(RegexOption.IGNORE_CASE) val integerRegex = "^-?\\d+\$".toRegex() - val decimalRegex = "^-?\\d*\\.\\d+\$".toRegex() - val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() // YYYY-MM-DD - val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() // YYYY-MM-DDThh:mm:ssZ - val stringRegex = "^[\\w\\s]+\$".toRegex() - val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() // e.g., "70 kg" - val codeableConceptRegex = "^[\\w\\s]+\$".toRegex() // Simplified for now - val codingRegex = "^\\w+\\|\$".toRegex() // Simplified for now - val referenceRegex = "^\\w+/\\w+\$".toRegex() // e.g., "Patient/123" - val periodRegex = "^\\d{4}-\\d{2}-\\d{2}\\/\\d{4}-\\d{2}-\\d{2}\$".toRegex() // e.g., "2023-01-01/2023-12-31" - val timingRegex = "^\\d+[a-zA-Z]+\$".toRegex() // Simplified for now - val rangeRegex = "^\\d+-\\d+\$".toRegex() // e.g., "10-20" - val annotationRegex = """^.*\s+\S+""".toRegex() // A basic regex for general text or comments - val attachmentRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 encoded string (could be more complex) - val base64BinaryRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Similar to attachment, but could have specific markers - val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // Regex for phone numbers (E.164 format) - val humanNameRegex = """^[A-Za-z\s'-]+$""".toRegex() // Simple regex for names - val addressRegex = """^\d+\s[A-Za-z]+\s[A-Za-z]+""".toRegex() // Basic address pattern - val durationRegex = """^\d+\s(hour|minute|second|day)$""".toRegex() // Duration like "1 hour" - val moneyRegex = """^\d+(\.\d{2})?\s[A-Z]{3}$""".toRegex() // Money format like "100.00 USD" - val ratioRegex = """^\d+:\d+$""".toRegex() // Simple ratio like "1:1000" - val signatureRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 signature - val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() // Identifier format - val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() // Simple URI format - val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() // UUID format - val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() // ISO 8601 instant format - val narrativeRegex = """.*<\/div>""".toRegex() // Narrative XHTML content - val sampledDataRegex = """^.*\s+\S+""".toRegex() // Placeholder regex for complex observation data - val backboneElementRegex = """^.*$""".toRegex() // Catch-all for complex structures (requires specific context) + val decimalRegex = "^-?\\d+\\.\\d+\$".toRegex() + val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() + val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() + val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() + val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() + val codingRegex = "^\\w+\\|\$".toRegex() + val referenceRegex = """^[A-Za-z]+\/[A-Za-z0-9\-\.]{1,64}$""".toRegex() + val periodRegex = "^\\d{4}-\\d{2}-\\d{2}/\\d{4}-\\d{2}-\\d{2}\$".toRegex() + val rangeRegex = "^\\d+-\\d+\$".toRegex() + val annotationRegex = """^[\w\s]+\:\s.*""".toRegex() + val base64BinaryRegex = """^[A-Za-z0-9+/=]{10,127}$""".toRegex() // General Base64 with length constraints + val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // International phone numbers (E.164 format) + val humanNameRegex = """^[A-Z][a-zA-Z]*(?:[\s'-][A-Z][a-zA-Z]*)*$""".toRegex() // Improved regex + val addressRegex = """^\d+\s[A-Za-z0-9\s\.,'-]+$""".toRegex() // Updated regex + val durationRegex = """^\d+\s?(s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days|w|week|weeks)$""".toRegex() + val moneyRegex = """^\d+(\.\d{1,2})?\s[A-Z]{3}$""".toRegex() // Updated regex + val ratioRegex = """^\d+:\d+$""".toRegex() // Updated regex + val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() + val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() + val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() + val narrativeRegex = """.*<\/div>$""".toRegex() + // Detect and Return FHIR Data Type return when { + uuidRegex.matches(cleanedValue) -> "Uuid" + referenceRegex.matches(cleanedValue) -> "Reference" + instantRegex.matches(cleanedValue) -> "Instant" + dateTimeRegex.matches(cleanedValue) -> "DateTime" + dateRegex.matches(cleanedValue) -> "Date" + uriRegex.matches(cleanedValue) -> "Uri" booleanRegex.matches(cleanedValue) -> "Boolean" integerRegex.matches(cleanedValue) -> "Integer" decimalRegex.matches(cleanedValue) -> "Decimal" - dateRegex.matches(cleanedValue) -> "Date" - dateTimeRegex.matches(cleanedValue) -> "DateTime" - quantityRegex.matches(input) -> "Quantity" - codeableConceptRegex.matches(input) -> "CodeableConcept" - codingRegex.matches(input) -> "Coding" - identifierRegex.matches(input) -> "Identifier" - referenceRegex.matches(input) -> "Reference" - periodRegex.matches(input) -> "Period" - timingRegex.matches(input) -> "Timing" - rangeRegex.matches(input) -> "Range" - stringRegex.matches(input) -> "String" - annotationRegex.matches(input) -> "Annotation" - attachmentRegex.matches(input) -> "Attachment" - base64BinaryRegex.matches(input) -> "Base64Binary" - contactPointRegex.matches(input) -> "ContactPoint" - humanNameRegex.matches(input) -> "HumanName" - addressRegex.matches(input) -> "Address" - durationRegex.matches(input) -> "Duration" - moneyRegex.matches(input) -> "Money" - ratioRegex.matches(input) -> "Ratio" - signatureRegex.matches(input) -> "Signature" - identifierRegex.matches(input) -> "Identifier" - uriRegex.matches(input) -> "Uri" - uuidRegex.matches(input) -> "Uuid" - instantRegex.matches(input) -> "Instant" - narrativeRegex.matches(input) -> "Narrative" - sampledDataRegex.matches(input) -> "SampledData" - backboneElementRegex.matches(input) -> "BackboneElement" + periodRegex.matches(cleanedValue) -> "Period" + rangeRegex.matches(cleanedValue) -> "Range" + moneyRegex.matches(cleanedValue) -> "Money" + durationRegex.matches(cleanedValue) -> "Duration" + ratioRegex.matches(cleanedValue) -> "Ratio" + quantityRegex.matches(cleanedValue) -> "Quantity" + humanNameRegex.matches(cleanedValue) -> "HumanName" + contactPointRegex.matches(cleanedValue) -> "ContactPoint" + base64BinaryRegex.matches(cleanedValue) -> "Base64Binary" + annotationRegex.matches(cleanedValue) -> "Annotation" + addressRegex.matches(cleanedValue) -> "Address" + identifierRegex.matches(cleanedValue) -> "Identifier" + codingRegex.matches(cleanedValue) -> "Coding" + narrativeRegex.matches(cleanedValue) -> "Narrative" else -> "String" } } diff --git a/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt b/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt new file mode 100644 index 00000000..4c91ac85 --- /dev/null +++ b/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt @@ -0,0 +1,96 @@ +package utils + +import org.junit.jupiter.api.Assertions.assertEquals +import org.smartregister.fhir.structuremaptool.determineFhirDataType +import kotlin.test.Test + +class DetermineFhirDataTypeTest { + + @Test + fun testDetermineFhirDataType() { + // Test Null or Empty Input + assertEquals("Invalid Input: Null or Empty String", determineFhirDataType(null)) + assertEquals("Invalid Input: Null or Empty String", determineFhirDataType("")) + + // Test Boolean + assertEquals("Boolean", determineFhirDataType("true")) + assertEquals("Boolean", determineFhirDataType("false")) + + // Test Integer + assertEquals("Integer", determineFhirDataType("123")) + assertEquals("Integer", determineFhirDataType("-456")) + + // Test Decimal + assertEquals("Decimal", determineFhirDataType("123.456")) + assertEquals("Decimal", determineFhirDataType("-0.789")) + + // Test Date + assertEquals("Date", determineFhirDataType("2023-08-23")) + + // Test DateTime + assertEquals("DateTime", determineFhirDataType("2023-08-23T14:30:00+01:00")) + + // Test Instant + assertEquals("Instant", determineFhirDataType("2023-08-23T14:30:00.123Z")) + + // Test Quantity + assertEquals("Quantity", determineFhirDataType("70 kg")) + + // Test Coding + assertEquals("Coding", determineFhirDataType("12345|")) + + // Test Reference + assertEquals("Reference", determineFhirDataType("Patient/123")) + + // Test Period + assertEquals("Period", determineFhirDataType("2023-01-01/2023-12-31")) + + // Test Range + assertEquals("Range", determineFhirDataType("10-20")) + + // Test Annotation + assertEquals("Annotation", determineFhirDataType("Note: Patient is recovering well")) + + // Test Attachment + + // Test Base64Binary + assertEquals("Base64Binary", determineFhirDataType("QmFzZTY0QmluYXJ5")) + + // Test ContactPoint + assertEquals("ContactPoint", determineFhirDataType("+123456789")) + + // Test HumanName + assertEquals("HumanName", determineFhirDataType("John Doe")) + + // Test Address + assertEquals("Address", determineFhirDataType("123 Main Street")) + + // Test Duration + assertEquals("Duration", determineFhirDataType("1 hour")) + + // Test Money + assertEquals("Money", determineFhirDataType("100.00 USD")) + + // Test Ratio + assertEquals("Ratio", determineFhirDataType("1:1000")) + + // Test Signature + // Test Identifier + assertEquals("Identifier", determineFhirDataType("AB123-45")) + + // Test Uri + assertEquals("Uri", determineFhirDataType("https://example.com")) + + // Test Uuid + assertEquals("Uuid", determineFhirDataType("123e4567-e89b-12d3-a456-426614174000")) + + // Test Narrative + assertEquals( + "Narrative", + determineFhirDataType("
Patient narrative
") + ) + + // Test String as Default Case + assertEquals("String", determineFhirDataType("Unmatched string")) + } +} \ No newline at end of file From ee646676481eab86aa84f6ff688a10b4a60ea955 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Mon, 23 Sep 2024 10:21:37 +0300 Subject: [PATCH 3/9] Resolve conflicts --- sm-gen/gradlew | 0 .../FhirPathEngineHostServices.kt | 150 +-- .../Main.kt | 557 +++++------ .../TransformSupportServices.kt | 87 +- .../Utils.kt | 904 ++++++++---------- .../FhirPathEngineHostServicesTest.kt | 140 +-- 6 files changed, 869 insertions(+), 969 deletions(-) mode change 100644 => 100755 sm-gen/gradlew diff --git a/sm-gen/gradlew b/sm-gen/gradlew old mode 100644 new mode 100755 diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt index 5b7979c2..fd4f5c15 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/FhirPathEngineHostServices.kt @@ -1,97 +1,99 @@ package org.smartregister.fhir.structuremaptool -import org.hl7.fhir.r4.model.* +import org.hl7.fhir.r4.model.Base +import org.hl7.fhir.r4.model.TypeDetails +import org.hl7.fhir.r4.model.ValueSet import org.hl7.fhir.r4.utils.FHIRPathEngine import org.slf4j.LoggerFactory /* -* Resolves constants defined in the fhir path expressions beyond those defined in the specification -*/ -internal object FHIRPathEngineHostServices : FHIRPathEngine.IEvaluationContext { + * Resolves constants defined in the fhir path expressions beyond those defined in the specification + */ +internal object FhirPathEngineHostServices : FHIRPathEngine.IEvaluationContext { - private val logger = LoggerFactory.getLogger(FHIRPathEngineHostServices::class.java) + private val logger = LoggerFactory.getLogger(FhirPathEngineHostServices::class.java) - // Cache to store resolved constants - private val constantCache = mutableMapOf() + // Cache to store resolved constants + private val constantCache = mutableMapOf() - // Cache for function details - private val functionCache = mutableMapOf() + // Cache for function details + private val functionCache = + mutableMapOf() - // Cache for resolved references - private val referenceCache = mutableMapOf() + // Cache for resolved references + private val referenceCache = mutableMapOf() - // Cache for value sets - private val valueSetCache = mutableMapOf() + // Cache for value sets + private val valueSetCache = mutableMapOf() - override fun resolveConstant(appContext: Any?, name: String?, beforeContext: Boolean): Base? { - if (name == null) return null + override fun resolveConstant(appContext: Any?, name: String?, beforeContext: Boolean): Base? { + if (name == null) return null - return constantCache.getOrPut(name) { - (appContext as? Map<*, *>)?.get(name) as? Base - } - } - - override fun resolveConstantType(appContext: Any?, name: String?): TypeDetails { - // Improved logging with null check - logger.info("Resolving constant type for: ${name ?: "null"}") - - if (name.isNullOrEmpty()) { - logger.warn("Cannot resolve constant type for a null or empty string.") - throw IllegalArgumentException("Constant name cannot be null or empty.") - } - - // Placeholder for actual implementation - throw UnsupportedOperationException("resolveConstantType is not yet implemented.") - } + return constantCache.getOrPut(name) { (appContext as? Map<*, *>)?.get(name) as? Base } + } + override fun resolveConstantType(appContext: Any?, name: String?): TypeDetails { + // Improved logging with null check + logger.info("Resolving constant type for: ${name ?: "null"}") - override fun log(argument: String?, focus: MutableList?): Boolean { - logger.info("Logging argument: $argument with focus: $focus") - return true + if (name.isNullOrEmpty()) { + logger.warn("Cannot resolve constant type for a null or empty string.") + throw IllegalArgumentException("Constant name cannot be null or empty.") } - override fun resolveFunction(functionName: String?): FHIRPathEngine.IEvaluationContext.FunctionDetails { - logger.info("Resolving function: ${functionName ?: "Unknown"}") - return functionCache.getOrPut(functionName ?: "") { - throw UnsupportedOperationException("Function $functionName is not yet implemented.") - } + // Placeholder for actual implementation + throw UnsupportedOperationException("resolveConstantType is not yet implemented.") + } + + override fun log(argument: String?, focus: MutableList?): Boolean { + logger.info("Logging argument: $argument with focus: $focus") + return true + } + + override fun resolveFunction( + functionName: String? + ): FHIRPathEngine.IEvaluationContext.FunctionDetails { + logger.info("Resolving function: ${functionName ?: "Unknown"}") + return functionCache.getOrPut(functionName ?: "") { + throw UnsupportedOperationException("Function $functionName is not yet implemented.") } - - override fun checkFunction( - appContext: Any?, - functionName: String?, - parameters: MutableList? - ): TypeDetails { - logger.info("Checking function: $functionName with parameters: $parameters") - throw UnsupportedOperationException("checkFunction is not yet implemented.") + } + + override fun checkFunction( + appContext: Any?, + functionName: String?, + parameters: MutableList?, + ): TypeDetails { + logger.info("Checking function: $functionName with parameters: $parameters") + throw UnsupportedOperationException("checkFunction is not yet implemented.") + } + + override fun executeFunction( + appContext: Any?, + focus: MutableList?, + functionName: String?, + parameters: MutableList>?, + ): MutableList { + logger.info("Executing function: $functionName with parameters: $parameters") + throw UnsupportedOperationException("executeFunction is not yet implemented.") + } + + override fun resolveReference(appContext: Any?, url: String?): Base { + logger.info("Resolving reference for URL: $url") + return referenceCache.getOrPut(url ?: "") { + throw UnsupportedOperationException("resolveReference is not yet implemented.") } + } - override fun executeFunction( - appContext: Any?, - focus: MutableList?, - functionName: String?, - parameters: MutableList>? - ): MutableList { - logger.info("Executing function: $functionName with parameters: $parameters") - throw UnsupportedOperationException("executeFunction is not yet implemented.") - } - - override fun resolveReference(appContext: Any?, url: String?): Base { - logger.info("Resolving reference for URL: $url") - return referenceCache.getOrPut(url ?: "") { - throw UnsupportedOperationException("resolveReference is not yet implemented.") - } - } - - override fun conformsToProfile(appContext: Any?, item: Base?, url: String?): Boolean { - logger.info("Checking if item conforms to profile: $url") - throw UnsupportedOperationException("conformsToProfile is not yet implemented.") - } + override fun conformsToProfile(appContext: Any?, item: Base?, url: String?): Boolean { + logger.info("Checking if item conforms to profile: $url") + throw UnsupportedOperationException("conformsToProfile is not yet implemented.") + } - override fun resolveValueSet(appContext: Any?, url: String?): ValueSet { - logger.info("Resolving ValueSet for URL: $url") - return valueSetCache.getOrPut(url ?: "") { - throw UnsupportedOperationException("resolveValueSet is not yet implemented.") - } + override fun resolveValueSet(appContext: Any?, url: String?): ValueSet { + logger.info("Resolving ValueSet for URL: $url") + return valueSetCache.getOrPut(url ?: "") { + throw UnsupportedOperationException("resolveValueSet is not yet implemented.") } + } } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt index 9b99c0ed..00e7f137 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt @@ -6,26 +6,25 @@ import ca.uhn.fhir.parser.IParser import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.prompt +import java.io.File +import java.io.FileInputStream +import java.nio.charset.Charset +import java.util.* import org.apache.commons.io.FileUtils import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.Row -import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory import org.hl7.fhir.r4.context.SimpleWorkerContext -import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion -import java.io.File -import java.io.FileInputStream -import java.nio.charset.Charset -import java.util.* fun main(args: Array) { - Application().main(args) + Application().main(args) } /*fun main(args: Array) { @@ -37,7 +36,6 @@ fun main(args: Array) { }*/ - /* REMAINING TASKS @@ -48,348 +46,297 @@ REMAINING TASKS */ class Application : CliktCommand() { - val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") - val questionnairefile : String by option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") - - - override fun run() { - // Create a map of Resource -> questionnaire name or path -> value - // For each resource loop through creating or adding the correct instructions - - lateinit var questionnaireResponse: QuestionnaireResponse - val contextR4 = FhirContext.forR4() - val fhirJsonParser = contextR4.newJsonParser() - val questionnaire : Questionnaire = fhirJsonParser.parseResource(Questionnaire::class.java, FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset())) - val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) - if (questionnaireResponseFile.exists()) { - questionnaireResponse = fhirJsonParser.parseResource(QuestionnaireResponse::class.java, questionnaireResponseFile.readText(Charset.defaultCharset())) - } else { - println("File not found: questionnaire-response.json") - } - - // reads the xls - val xlsFile = FileInputStream(xlsfile) - val xlWb = WorkbookFactory.create(xlsFile) - - // Validate resources and paths in the XLS sheet - validateResourcesAndPaths(xlWb) + val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") + val questionnairefile: String by + option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") + + override fun run() { + // Create a map of Resource -> questionnaire name or path -> value + // For each resource loop through creating or adding the correct instructions + + lateinit var questionnaireResponse: QuestionnaireResponse + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire: Questionnaire = + fhirJsonParser.parseResource( + Questionnaire::class.java, + FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) + ) + val questionnaireResponseFile = + File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) + if (questionnaireResponseFile.exists()) { + questionnaireResponse = + fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, + questionnaireResponseFile.readText(Charset.defaultCharset()) + ) + } else { + println("File not found: questionnaire-response.json") + } - // Fix groups calling sequence - fixGroupCallingSequence(xlWb) - // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. RiskFlag in the previous XLSX was not valid - // TODO: Check that all the path's and other entries in the excel sheet are valid - // TODO: Add instructions for adding embedded classes like `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices + // reads the xls + val xlsFile = FileInputStream(xlsfile) + val xlWb = WorkbookFactory.create(xlsFile) - /* + // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. + // RiskFlag in the previous XLSX was not valid + // TODO: Check that all the path's and other entries in the excel sheet are valid + // TODO: Add instructions for adding embedded classes like + // `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices - READ THE SETTINGS SHEET + /* - */ - val settingsWorkbook = xlWb.getSheet("Settings") - var questionnaireId : String? = null + READ THE SETTINGS SHEET - for (i in 0..settingsWorkbook.lastRowNum) { - val cell = settingsWorkbook.getRow(i).getCell(0) + */ + val settingsWorkbook = xlWb.getSheet("Settings") + var questionnaireId: String? = null - if (cell.stringCellValue == "questionnaire-id") { - questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue - } - } + for (i in 0..settingsWorkbook.lastRowNum) { + val cell = settingsWorkbook.getRow(i).getCell(0) - /* + if (cell.stringCellValue == "questionnaire-id") { + questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue + } + } - END OF READ SETTINGS SHEET + /* - */ + END OF READ SETTINGS SHEET - /* - TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first - We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! - */ - val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } - if(questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()){ + */ - val sb = StringBuilder() - val structureMapHeader = """ + /* + TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first + We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! + */ + val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } + if (questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()) { + val sb = StringBuilder() + val structureMapHeader = + """ map "http://hl7.org/fhir/StructureMap/$questionnaireId" = '${questionnaireId.clean()}' uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireReponse" as source uses "http://hl7.org/fhir/StructureDefinition/Bundle" as target - """.trimIndent() + """ + .trimIndent() - val structureMapBody = """ + val structureMapBody = + """ group ${questionnaireId.clean()}(source src : QuestionnaireResponse, target bundle: Bundle) { src -> bundle.id = uuid() "rule_c"; src -> bundle.type = 'collection' "rule_b"; - src -> bundle.entry as entry then """.trimIndent() - - /* - - Create a mapping of COLUMN_NAMES to COLUMN indexes - - */ - //val mapColumns - - - val lineNos = 1 - var firstResource = true - val extractionResources = hashMapOf() - val resourceConversionInstructions = hashMapOf>() - - // Group the rules according to the resource - val fieldMappingsSheet = xlWb.getSheet("Field Mappings") - fieldMappingsSheet.forEachIndexed { index, row -> - if (index == 0) return@forEachIndexed - - if (row.isEmpty()) { - return@forEachIndexed - } - - - val instruction = row.getInstruction() - val xlsId = instruction.responseFieldId - val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) - if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { - resourceConversionInstructions.computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) - .add(instruction) - } - } - //val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as Resource - -// -// // Perform the extraction for the row -// /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) -// -// extractionResources[resourceName + resourceIndex] = resource*/ - - sb.append(structureMapHeader) - sb.appendNewLine().appendNewLine().appendNewLine() - sb.append(structureMapBody) - - // Fix the questions path - val questionsPath = getQuestionsPath(questionnaire) - - // TODO: Generate the links to the group names here - var index = 0 - var len = resourceConversionInstructions.size - var resourceName = "" - resourceConversionInstructions.forEach { entry -> - val resourceName = entry.key.replaceFirstChar { it.titlecase(Locale.getDefault()) } - if (index++ != 0) sb.append(",") - sb.append("Extract$resourceName(src, bundle)") - } - sb.append(""" "rule_a";""".trimMargin()) - sb.appendNewLine().append("}") - - sb.appendNewLine().appendNewLine().appendNewLine() - - resourceConversionInstructions.forEach { - Group(it, sb, questionsPath).generateGroup(questionnaireResponse) - } - - val structureMapString = sb.toString() - try { - val simpleWorkerContext = SimpleWorkerContext().apply { - setExpansionProfile(Parameters()) - isCanRunWithoutTerminology = true - } - val transformSupportServices = TransformSupportServices(simpleWorkerContext) - val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) - val structureMap = scu.parse(structureMapString, questionnaireId.clean()) - // DataFormatException | FHIRLexerException - - try{ - val bundle = Bundle() - scu.transform(contextR4, questionnaireResponse, structureMap, bundle) - val jsonParser = FhirContext.forR4().newJsonParser() - - println(jsonParser.encodeResourceToString(bundle)) - } catch (e:Exception){ - e.printStackTrace() - } - - } catch (ex: Exception) { - println("The generated StructureMap has a formatting error") - ex.printStackTrace() - } - - var finalStructureMap = sb.toString() - finalStructureMap = finalStructureMap.addIdentation() - println(finalStructureMap) - writeStructureMapOutput(sb.toString().addIdentation()) - } - - } - private fun validateResourcesAndPaths(workbook: Workbook) { - val fieldMappingsSheet = workbook.getSheet("Field Mappings") - fieldMappingsSheet.forEachIndexed { index, row -> - if (index == 0) return@forEachIndexed - - val resourceName = row.getCellAsString(2) - val fieldPath = row.getCellAsString(4) - - if (!isValidResource(resourceName)) { - throw IllegalArgumentException("Invalid resource name: $resourceName") - } - - if (!isValidPath(fieldPath)) { - throw IllegalArgumentException("Invalid field path: $fieldPath") - } + src -> bundle.entry as entry then + """ + .trimIndent() + + val lineNos = 1 + var firstResource = true + val extractionResources = hashMapOf() + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWb.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + + if (row.isEmpty()) { + return@forEachIndexed } - } - private fun isValidResource(resourceName: String?): Boolean { - // Implement logic to validate resource names - // This can be a list of known valid resource names, or a more complex validation - return resourceName != null && resourceName.isNotEmpty() - } - - private fun isValidPath(path: String?): Boolean { - // Implement logic to validate paths - // This can involve checking against known paths or ensuring the format is correct - return path != null && path.isNotEmpty() - } - - private fun fixGroupCallingSequence(workbook: Workbook) { - // Implement logic to fix group calling sequences - // Detect and handle cyclic dependencies, using topological sorting or other methods - // You can throw an exception if a cyclic dependency is detected - } - - private fun groupRulesByResource(workbook: Workbook, questionnaireResponseItemIds: List): Map> { - val fieldMappingsSheet = workbook.getSheet("Field Mappings") - val resourceConversionInstructions = hashMapOf>() - - 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 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) } - - return resourceConversionInstructions - } - - fun Row.getInstruction() : Instruction { - return Instruction().apply { - responseFieldId = getCell(0) ?.stringCellValue - constantValue = getCellAsString(1) - resource = getCell(2).stringCellValue - resourceIndex = getCell(3) ?.numericCellValue?.toInt() ?: 0 - fieldPath = getCell(4) ?.stringCellValue ?: "" - fullFieldPath = fieldPath - field = getCell(5) ?.stringCellValue - conversion = getCell(6) ?.stringCellValue - fhirPathStructureMapFunctions = getCell(7) ?.stringCellValue + } + // val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as + // Resource + + // Perform the extraction for the row + /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) + + extractionResources[resourceName + resourceIndex] = resource*/ + + sb.append(structureMapHeader) + sb.appendNewLine().appendNewLine().appendNewLine() + sb.append(structureMapBody) + + // Fix the questions path + val questionsPath = getQuestionsPath(questionnaire) + + // TODO: Generate the links to the group names here + var index = 0 + var len = resourceConversionInstructions.size + var resourceName = "" + resourceConversionInstructions.forEach { entry -> + resourceName = + entry.key.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + if (index++ != 0) sb.append(",") + if (resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") + } + sb.append(""" "rule_a";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + + // Add the embedded instructions + val groupNames = mutableListOf() + + sb.appendNewLine().appendNewLine().appendNewLine() + + resourceConversionInstructions.forEach { + Group(it, sb, questionsPath).generateGroup(questionnaireResponse) + } + + val structureMapString = sb.toString() + try { + val simpleWorkerContext = + SimpleWorkerContext().apply { + setExpansionProfile(Parameters()) + isCanRunWithoutTerminology = true + } + val transformSupportServices = TransformSupportServices(simpleWorkerContext) + val scu = + org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) + val structureMap = scu.parse(structureMapString, questionnaireId.clean()) + // DataFormatException | FHIRLexerException + + try { + val bundle = Bundle() + scu.transform(contextR4, questionnaireResponse, structureMap, bundle) + val jsonParser = FhirContext.forR4().newJsonParser() + + println(jsonParser.encodeResourceToString(bundle)) + } catch (e: Exception) { + e.printStackTrace() } + } catch (ex: Exception) { + println("The generated StructureMap has a formatting error") + ex.printStackTrace() + } + + var finalStructureMap = sb.toString() + finalStructureMap = finalStructureMap.addIndentation() + println(finalStructureMap) + writeStructureMapOutput(sb.toString().addIndentation()) } - - fun Row.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.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 String.clean() : String { - return this.replace("-", "") - .replace("_", "") - .replace(" ", "") - } + fun Row.isEmpty(): Boolean { + return getCell(0) == null && getCell(1) == null && getCell(2) == null + } + fun String.clean(): String { + return this.replace("-", "").replace("_", "").replace(" ", "") + } } class Instruction { - var responseFieldId : String? = null - var constantValue: String? = null - var resource: String = "" - var resourceIndex: Int = 0 - var fieldPath: String = "" - var field: String? = null - var conversion : String? = null - var fhirPathStructureMapFunctions: String? = null + var responseFieldId: String? = null + var constantValue: String? = null + var resource: String = "" + var resourceIndex: Int = 0 + var fieldPath: String = "" + var field: String? = null + var conversion: String? = null + var fhirPathStructureMapFunctions: String? = null + // TODO: Clean the following properties + var fullFieldPath = "" - // TODO: Clean the following properties - var fullFieldPath = "" - fun fullPropertyPath() : String = "$resource.$fullFieldPath" + fun fullPropertyPath(): String = "$resource.$fullFieldPath" - fun searchKey() = resource + resourceIndex + fun searchKey() = resource + resourceIndex } fun Instruction.copyFrom(instruction: Instruction) { - constantValue = instruction.constantValue - resource = instruction.resource - resourceIndex = instruction.resourceIndex - fieldPath = instruction.fieldPath - fullFieldPath = instruction.fullFieldPath - field = instruction.field - conversion = instruction.conversion - fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions + constantValue = instruction.constantValue + resource = instruction.resource + resourceIndex = instruction.resourceIndex + fieldPath = instruction.fieldPath + fullFieldPath = instruction.fullFieldPath + field = instruction.field + conversion = instruction.conversion + fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions } - -fun String.addIdentation() : String { - var currLevel = 0 - - val lines = split("\n") - - val sb = StringBuilder() - lines.forEach { line -> - if (line.endsWith("{")) { - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - currLevel++ - } else if (line.startsWith("}")) { - currLevel-- - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - } else { - sb.append(line.addIdentation(currLevel)) - sb.appendNewLine() - } +fun String.addIndentation(): String { + var currLevel = 0 + + val lines = split("\n") + + val sb = StringBuilder() + lines.forEach { line -> + if (line.endsWith("{")) { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + currLevel++ + } else if (line.startsWith("}")) { + currLevel-- + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() + } else { + sb.append(line.addIndentation(currLevel)) + sb.appendNewLine() } + } - return sb.toString() + return sb.toString() } -fun String.addIdentation(times: Int) : String { - var processedString = "" - for (k in 1..times) { - processedString += "\t" - } +fun String.addIndentation(times: Int): String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } - processedString += this - return processedString + processedString += this + return processedString } -fun writeStructureMapOutput( structureMap: String){ - File("generated-structure-map.txt").writeText(structureMap.addIdentation()) - val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) - val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) - contextR5.setExpansionProfile(Parameters()) - contextR5.isCanRunWithoutTerminology = true - val transformSupportServices = TransformSupportServices(contextR5) - val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) - val map = scu.parse(structureMap, "LocationRegistration") - val iParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) - val mapString = iParser.encodeResourceToString(map) - File("generated-json-map.json").writeText(mapString) +fun writeStructureMapOutput(structureMap: String) { + File("generated-structure-map.txt").writeText(structureMap.addIndentation()) + val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) + val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) + contextR5.setExpansionProfile(Parameters()) + contextR5.isCanRunWithoutTerminology = true + val transformSupportServices = TransformSupportServices(contextR5) + val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) + val map = scu.parse(structureMap, "LocationRegistration") + val iParser: IParser = + FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) + val mapString = iParser.encodeResourceToString(map) + File("generated-json-map.json").writeText(mapString) } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt index 70a007f4..e638a22b 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt @@ -1,9 +1,7 @@ package org.smartregister.fhir.structuremaptool -import org.hl7.fhir.r4.context.SimpleWorkerContext -import org.hl7.fhir.r4.utils.StructureMapUtilities.ITransformerServices - import org.hl7.fhir.exceptions.FHIRException +import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.Coding @@ -18,55 +16,56 @@ import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.RiskAssessment.RiskAssessmentPredictionComponent import org.hl7.fhir.r4.model.Timing import org.hl7.fhir.r4.terminologies.ConceptMapEngine +import org.hl7.fhir.r4.utils.StructureMapUtilities.ITransformerServices class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorkerContext) : - ITransformerServices { + ITransformerServices { - val outputs: MutableList = mutableListOf() + val outputs: MutableList = mutableListOf() - override fun log(message: String) { - System.out.println(message) - } + override fun log(message: String) { + println(message) + } - @Throws(FHIRException::class) - override fun createType(appInfo: Any, name: String): Base { - return when (name) { - "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() - "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() - "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() - "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() - "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() - "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() - "Encounter_Participant" -> Encounter.EncounterParticipantComponent() - "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() - "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() - "Patient_Link" -> Patient.PatientLinkComponent() - "Timing_Repeat" -> Timing.TimingRepeatComponent() - "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() - "Group_Characteristic" -> Group.GroupCharacteristicComponent() - "Observation_Component" -> Observation.ObservationComponentComponent() - else -> ResourceFactory.createResourceOrType(name) - } + @Throws(FHIRException::class) + override fun createType(appInfo: Any, name: String): Base { + return when (name) { + "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() + "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() + "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() + "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() + "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() + "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() + "Encounter_Participant" -> Encounter.EncounterParticipantComponent() + "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() + "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() + "Patient_Link" -> Patient.PatientLinkComponent() + "Timing_Repeat" -> Timing.TimingRepeatComponent() + "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() + "Group_Characteristic" -> Group.GroupCharacteristicComponent() + "Observation_Component" -> Observation.ObservationComponentComponent() + else -> ResourceFactory.createResourceOrType(name) } + } - override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { - if (atRootofTransform) outputs.add(res) - return res - } + override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { + if (atRootofTransform) outputs.add(res) + return res + } - @Throws(FHIRException::class) - override fun translate(appInfo: Any, source: Coding, conceptMapUrl: String): Coding { - val cme = ConceptMapEngine(simpleWorkerContext) - return cme.translate(source, conceptMapUrl) - } + @Throws(FHIRException::class) + override fun translate(appInfo: Any, source: Coding, conceptMapUrl: String): Coding { + val cme = ConceptMapEngine(simpleWorkerContext) + return cme.translate(source, conceptMapUrl) + } - @Throws(FHIRException::class) - override fun resolveReference(appContext: Any, url: String): Base { - throw FHIRException("resolveReference is not supported yet") - } + @Throws(FHIRException::class) + override fun resolveReference(appContext: Any, url: String): Base { + throw FHIRException("resolveReference is not supported yet") + } - @Throws(FHIRException::class) - override fun performSearch(appContext: Any, url: String): List { - throw FHIRException("performSearch is not supported yet") - } + @Throws(FHIRException::class) + override fun performSearch(appContext: Any, url: String): List { + throw FHIRException("performSearch is not supported yet") + } } diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt index d814d420..843944d1 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt @@ -2,6 +2,8 @@ package org.smartregister.fhir.structuremaptool import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType import org.apache.poi.ss.usermodel.Row import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Enumeration @@ -11,600 +13,534 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.Type import org.hl7.fhir.r4.utils.FHIRPathEngine -import java.lang.reflect.Field -import java.lang.reflect.ParameterizedType -import kotlin.reflect.KClass // Get the hl7 resources val contextR4 = FhirContext.forR4() val fhirResources = contextR4.resourceTypes + fun getQuestionsPath(questionnaire: Questionnaire): HashMap { - val questionsMap = hashMapOf() + val questionsMap = hashMapOf() - questionnaire.item.forEach { itemComponent -> - getQuestionNames("", itemComponent, questionsMap) - } - return questionsMap + questionnaire.item.forEach { itemComponent -> getQuestionNames("", itemComponent, questionsMap) } + return questionsMap } -fun getQuestionNames(parentName: String, item: QuestionnaireItemComponent, questionsMap: HashMap) { - val currParentName = if (parentName.isEmpty()) "" else parentName - questionsMap.put(item.linkId, currParentName) - - item.item.forEach { itemComponent -> - getQuestionNames(currParentName + ".where(linkId = '${item.linkId}').item", itemComponent, questionsMap) - } +fun getQuestionNames( + parentName: String, + item: QuestionnaireItemComponent, + questionsMap: HashMap +) { + val currParentName = if (parentName.isEmpty()) "" else parentName + questionsMap.put(item.linkId, currParentName) + + item.item.forEach { itemComponent -> + getQuestionNames( + currParentName + ".where(linkId = '${item.linkId}').item", + itemComponent, + questionsMap + ) + } } - class Group( - entry: Map.Entry>, - val stringBuilder: StringBuilder, - val questionsPath: HashMap + entry: Map.Entry>, + val stringBuilder: StringBuilder, + val questionsPath: HashMap, ) { - var lineCounter = 0 - var groupName = entry.key - val instructions = entry.value - - private fun generateReference(resourceName: String, resourceIndex: String): String { - // Generate the reference based on the resourceName and resourceIndex - val sb = StringBuilder() - sb.append("create('Reference') as reference then {") - sb.appendNewLine() - sb.append("src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))") - sb.append(""" "rule_d";""".trimMargin()) - sb.appendNewLine() - sb.append("}") - return sb.toString() + var lineCounter = 0 + var groupName = entry.key + val instructions = entry.value + + private fun generateReference(resourceName: String, resourceIndex: String): String { + // Generate the reference based on the resourceName and resourceIndex + val sb = StringBuilder() + sb.append("create('Reference') as reference then {") + sb.appendNewLine() + sb.append( + "src-> reference.reference = evaluate(bundle, \$this.entry.where(resourceType = '$resourceName/$resourceIndex'))" + ) + sb.append(""" "rule_d";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + return sb.toString() + } + + fun generateGroup(questionnaireResponse: QuestionnaireResponse) { + if (fhirResources.contains(groupName.dropLast(1))) { + val resourceName = instructions[0].resource + + // add target of reference to function if reference is not null + val structureMapFunctionHead = + "group Extract$groupName(source src : QuestionniareResponse, target bundle: Bundle) {" + stringBuilder.appendNewLine() + stringBuilder.append(structureMapFunctionHead).appendNewLine() + stringBuilder + .append( + "src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {" + ) + .appendNewLine() + + val mainNest = Nest() + mainNest.fullPath = "" + mainNest.name = "" + mainNest.resourceName = resourceName + + instructions.forEachIndexed { index, instruction -> mainNest.add(instruction) } + + mainNest.buildStructureMap(0, questionnaireResponse) + + stringBuilder.append("} ") + addRuleNo() + stringBuilder.appendNewLine() + stringBuilder.append("}") + stringBuilder.appendNewLine() + stringBuilder.appendNewLine() + } else { + println("$groupName is not a valid hl7 resource name") } - - fun generateGroup(questionnaireResponse: QuestionnaireResponse) { - if(fhirResources.contains(groupName.dropLast(1))){ - val resourceName = instructions[0].resource - - // add target of reference to function if reference is not null - val structureMapFunctionHead = "group Extract$groupName(source src : QuestionniareResponse, target bundle: Bundle) {" - stringBuilder.appendNewLine() - stringBuilder.append(structureMapFunctionHead) - .appendNewLine() - stringBuilder.append("src -> bundle.entry as entry, entry.resource = create('$resourceName') as entity1 then {") - .appendNewLine() - - val mainNest = Nest() - mainNest.fullPath = "" - mainNest.name = "" - mainNest.resourceName = resourceName - - instructions.forEachIndexed { index, instruction -> - mainNest.add(instruction) - } - - mainNest.buildStructureMap(0, questionnaireResponse) - - - stringBuilder.append("} ") - addRuleNo() - stringBuilder.appendNewLine() - stringBuilder.append("}") - stringBuilder.appendNewLine() - stringBuilder.appendNewLine() - } else{ - println("$groupName is not a valid hl7 resource name") + } + + fun addRuleNo() { + stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) + } + + fun Instruction.getPropertyPath(): String { + return questionsPath.getOrDefault(responseFieldId, "") + } + + fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { + // 1. If the answer is static/literal, just return it here + // TODO: We should infer the resource element and add the correct conversion or code to assign + // this correctly + if (constantValue != null) { + return when { + fieldPath == "id" -> "create('id') as id, id.value = '$constantValue'" + fieldPath == "rank" -> { + val constValue = constantValue!!.replace(".0", "") + "create('positiveInt') as rank, rank.value = '$constValue'" } + else -> "'$constantValue'" + } } - fun addRuleNo() { - stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) + // 2. If the answer is from the QuestionnaireResponse, get the ID of the item in the + // "Questionnaire Response Field Id" and + // get its value using FHIR Path expressions + if (responseFieldId != null) { + // TODO: Fix the 1st param inside the evaluate expression + var expression = + "${"$"}this.item${getPropertyPath()}.where(linkId = '$responseFieldId').answer.value" + // TODO: Fix these to use infer + if (fieldPath == "id" || fieldPath == "rank") { + expression = + "create('${if (fieldPath == "id") "id" else "positiveInt"}') as $fieldPath, $fieldPath.value = evaluate(src, $expression)" + } else { + // TODO: Infer the resource property type and answer to perform other conversions + // TODO: Extend this to cover other corner cases + if (expression.isCoding(questionnaireResponse) && fieldPath.isEnumeration(this)) { + expression = expression.replace("answer.value", "answer.value.code") + } else if (inferType(fullPropertyPath()) == "CodeableConcept") { + return "''" + } + expression = "evaluate(src, $expression)" + } + return expression } - fun Instruction.getPropertyPath(): String { - return questionsPath.getOrDefault(responseFieldId, "") + // 3. If it's a FHIR Path/StructureMap function, add the contents directly from here to the + // StructureMap + if (fhirPathStructureMapFunctions != null && fhirPathStructureMapFunctions!!.isNotEmpty()) { + // TODO: Fix the 2nd param inside the evaluate expression --> Not sure what this is but check + // this + return fhirPathStructureMapFunctions!! } - - fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { - - //1. If the answer is static/literal, just return it here - // TODO: We should infer the resource element and add the correct conversion or code to assign this correctly - if (constantValue != null) { - return when { - fieldPath == "id" -> "create('id') as id, id.value = '$constantValue'" - fieldPath == "rank" -> { - val constValue = constantValue!!.replace(".0", "") - "create('positiveInt') as rank, rank.value = '$constValue'" - } - else -> "'$constantValue'" - } - } - - // 2. If the answer is from the QuestionnaireResponse, get the ID of the item in the "Questionnaire Response Field Id" and - // get its value using FHIR Path expressions - if (responseFieldId != null) { - // TODO: Fix the 1st param inside the evaluate expression - var expression = "${"$"}this.item${getPropertyPath()}.where(linkId = '$responseFieldId').answer.value" - // TODO: Fix these to use infer - if (fieldPath == "id" || fieldPath == "rank") { - expression = "create('${if (fieldPath == "id") "id" else "positiveInt"}') as $fieldPath, $fieldPath.value = evaluate(src, $expression)" - } else { - - // TODO: Infer the resource property type and answer to perform other conversions - // TODO: Extend this to cover other corner cases - if (expression.isCoding(questionnaireResponse) && fieldPath.isEnumeration(this)) { - expression = expression.replace("answer.value", "answer.value.code") - } else if (inferType(fullPropertyPath()) == "CodeableConcept") { - return "''" - } - expression = "evaluate(src, $expression)" - } - return expression - } - - // 3. If it's a FHIR Path/StructureMap function, add the contents directly from here to the StructureMap - if (fhirPathStructureMapFunctions != null && fhirPathStructureMapFunctions!!.isNotEmpty()) { - // TODO: Fix the 2nd param inside the evaluate expression --> Not sure what this is but check this - return fhirPathStructureMapFunctions!! - } - // 4. If the answer is a conversion, (Assume this means it's being converted to a reference) - if (conversion != null && conversion!!.isNotBlank() && conversion!!.isNotEmpty()) { - println("current resource to reference is $conversion") - - val resourceName = conversion!!.replace("$", "") - var resourceIndex = conversion!!.replace("$$resourceName", "") - if (resourceIndex.isNotEmpty()) { - resourceIndex = "[$resourceIndex]" - } - val reference = generateReference(resourceName = resourceName, resourceIndex = resourceIndex) - return reference - } - - /* - 5. You can use $Resource eg $Patient to reference another resource being extracted here, - but how do we actually get its instance so that we can use it???? - This should be handled elsewhere - */ - - return "''" + // 4. If the answer is a conversion, (Assume this means it's being converted to a reference) + if (conversion != null && conversion!!.isNotBlank() && conversion!!.isNotEmpty()) { + println("current resource to reference is $conversion") + + val resourceName = conversion!!.replace("$", "") + var resourceIndex = conversion!!.replace("$$resourceName", "") + if (resourceIndex.isNotEmpty()) { + resourceIndex = "[$resourceIndex]" + } + val reference = generateReference(resourceName = resourceName, resourceIndex = resourceIndex) + return reference } + /* + 5. You can use $Resource eg $Patient to reference another resource being extracted here, + but how do we actually get its instance so that we can use it???? - This should be handled elsewhere + */ + + return "''" + } + + inner class Nest { + var instruction: Instruction? = null + + // We can change this to a linked list + val nests = ArrayList() + lateinit var name: String + lateinit var fullPath: String + lateinit var resourceName: String + + fun add(instruction: Instruction) { + /*if (instruction.fieldPath.startsWith(fullPath)) { + + }*/ + val remainingPath = instruction.fieldPath.replace(fullPath, "") + + remainingPath.run { + if (contains(".")) { + val parts = split(".") + val partName = parts[0].ifEmpty { parts[1] } + + // Search for the correct property to put this nested property + nests.forEach { + if (partName.startsWith(it.name)) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + if (index > 0 && index < parts.size - 1) { + newFieldPath += "." + } + } - inner class Nest { - var instruction: Instruction? = null - - // We can change this to a linked list - val nests = ArrayList() - lateinit var name: String - lateinit var fullPath: String - lateinit var resourceName: String - - fun add(instruction: Instruction) { - /*if (instruction.fieldPath.startsWith(fullPath)) { + fieldPath = newFieldPath + } - }*/ - val remainingPath = instruction.fieldPath.replace(fullPath, "") + it.add(nextInstruction) - remainingPath.run { - if (contains(".")) { - val parts = split(".") - val partName = parts[0].ifEmpty { - parts[1] - } + return@run + } + } - // Search for the correct property to put this nested property - nests.forEach { - if (partName.startsWith(it.name)) { - val nextInstruction = Instruction().apply { - copyFrom(instruction) - var newFieldPath = "" - parts.forEachIndexed { index, s -> - if (index != 0) { - newFieldPath += s - } - - if (index > 0 && index < parts.size - 1) { - newFieldPath += "." - } - } - - fieldPath = newFieldPath - } - - it.add(nextInstruction) - - return@run - } - } + // If no match is found, let's create a new one + val newNest = + Nest().apply { + name = partName - // If no match is found, let's create a new one - val newNest = Nest().apply { - name = partName - - fullPath = if (this@Nest.fullPath.isNotEmpty()) { - "${this@Nest.fullPath}.$partName" - } else { - partName - } - resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" - - if ((parts[0].isEmpty() && parts.size > 2) || (parts[0].isNotEmpty() && parts.size > 1)) { - val nextInstruction = Instruction().apply { - copyFrom(instruction) - var newFieldPath = "" - parts.forEachIndexed { index, s -> - if (index != 0) { - newFieldPath += s - } - } - - fieldPath = newFieldPath - } - add(nextInstruction) - } else { - this@apply.instruction = instruction - } - } - nests.add(newNest) + fullPath = + if (this@Nest.fullPath.isNotEmpty()) { + "${this@Nest.fullPath}.$partName" } else { - this@Nest.nests.add(Nest().apply { - name = remainingPath - fullPath = instruction.fieldPath - this@apply.instruction = instruction - resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" - }) + partName } + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + + if ( + (parts[0].isEmpty() && parts.size > 2) || (parts[0].isNotEmpty() && parts.size > 1) + ) { + val nextInstruction = + Instruction().apply { + copyFrom(instruction) + var newFieldPath = "" + parts.forEachIndexed { index, s -> + if (index != 0) { + newFieldPath += s + } + } + + fieldPath = newFieldPath + } + add(nextInstruction) + } else { + this@apply.instruction = instruction + } } + nests.add(newNest) + } else { + this@Nest.nests.add( + Nest().apply { + name = remainingPath + fullPath = instruction.fieldPath + this@apply.instruction = instruction + resourceName = inferType("${this@Nest.resourceName}.$fullPath") ?: "" + }, + ) } + } + } - fun buildStructureMap(currLevel: Int, questionnaireResponse: QuestionnaireResponse) { - if (instruction != null) { - val answerExpression = instruction?.getAnswerExpression(questionnaireResponse) - - if (answerExpression != null) { - if (answerExpression.isNotEmpty() && answerExpression.isNotBlank() && answerExpression != "''") { - val propertyType = inferType(instruction!!.fullPropertyPath()) - val answerType = answerExpression.getAnswerType(questionnaireResponse) - - if (propertyType != "Type" && answerType != propertyType && propertyType?.canHandleConversion( - answerType ?: "" - )?.not() == true && answerExpression.startsWith("evaluate") - ) { - println("Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType") - stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") - stringBuilder.append("create('${propertyType.getFhirType()}') as randomVal, randomVal.value = ") - stringBuilder.append(answerExpression) - addRuleNo() - stringBuilder.appendNewLine() - return - } - - stringBuilder.append("src -> entity$currLevel.${instruction!!.fieldPath} = ") - stringBuilder.append(answerExpression) - addRuleNo() - stringBuilder.appendNewLine() - } - } - } else if (nests.size > 0) { - //val resourceType = inferType("entity$currLevel.$name", instruction) - - 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 {") + 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) - } + 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, ) { - val fieldPath = row.getCell(4)?.stringCellValue ?: "" - val cellValue = row.getCell(0)?.stringCellValue ?: "" - - // Determine the target FHIR data type + row.forEachIndexed { index, cell -> + val cellValue = cell.stringCellValue + val fieldPath = row.getCell(4).stringCellValue val targetDataType = determineFhirDataType(cellValue) + structureMapBody.append("src -> entity.$fieldPath=") - // Generate the mapping line for the StructureMap - structureMapBody.append("src -> entity.$fieldPath = ") - - // Handle different data types when (targetDataType) { - "string" -> { - structureMapBody.append("create('string').value = '${cellValue.escapeQuotes()}'") - } - "integer" -> { - structureMapBody.append("create('integer').value = ${cellValue.toIntOrNull() ?: 0}") - } - "boolean" -> { - structureMapBody.append("create('boolean').value = ${cellValue.toBoolean()}") - } - "date" -> { - structureMapBody.append("create('date').value = '${cellValue}'") - } - // Add more cases for other FHIR types as needed - else -> { - structureMapBody.append("create('$targetDataType').value = '${cellValue.escapeQuotes()}'") - } + "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.append(";") -} - - -fun String.escapeQuotes(): String { - return this.replace("'", "\\'") + structureMapBody.appendNewLine() + } } -fun determineFhirDataType(input: String?): String { - if (input.isNullOrEmpty()) { - return "Invalid Input: Null or Empty String" - } - - val cleanedValue = input.trim() - - // Regular Expressions for FHIR Data Types - val booleanRegex = "^(true|false)\$".toRegex(RegexOption.IGNORE_CASE) - val integerRegex = "^-?\\d+\$".toRegex() - val decimalRegex = "^-?\\d+\\.\\d+\$".toRegex() - val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() - val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() - val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() - val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() - val codingRegex = "^\\w+\\|\$".toRegex() - val referenceRegex = """^[A-Za-z]+\/[A-Za-z0-9\-\.]{1,64}$""".toRegex() - val periodRegex = "^\\d{4}-\\d{2}-\\d{2}/\\d{4}-\\d{2}-\\d{2}\$".toRegex() - val rangeRegex = "^\\d+-\\d+\$".toRegex() - val annotationRegex = """^[\w\s]+\:\s.*""".toRegex() - val base64BinaryRegex = """^[A-Za-z0-9+/=]{10,127}$""".toRegex() // General Base64 with length constraints - val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // International phone numbers (E.164 format) - val humanNameRegex = """^[A-Z][a-zA-Z]*(?:[\s'-][A-Z][a-zA-Z]*)*$""".toRegex() // Improved regex - val addressRegex = """^\d+\s[A-Za-z0-9\s\.,'-]+$""".toRegex() // Updated regex - val durationRegex = """^\d+\s?(s|second|seconds|m|minute|minutes|h|hour|hours|d|day|days|w|week|weeks)$""".toRegex() - val moneyRegex = """^\d+(\.\d{1,2})?\s[A-Z]{3}$""".toRegex() // Updated regex - val ratioRegex = """^\d+:\d+$""".toRegex() // Updated regex - val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() - val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() - val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() - val narrativeRegex = """.*<\/div>$""".toRegex() - - // Detect and Return FHIR Data Type - return when { - uuidRegex.matches(cleanedValue) -> "Uuid" - referenceRegex.matches(cleanedValue) -> "Reference" - instantRegex.matches(cleanedValue) -> "Instant" - dateTimeRegex.matches(cleanedValue) -> "DateTime" - dateRegex.matches(cleanedValue) -> "Date" - uriRegex.matches(cleanedValue) -> "Uri" - booleanRegex.matches(cleanedValue) -> "Boolean" - integerRegex.matches(cleanedValue) -> "Integer" - decimalRegex.matches(cleanedValue) -> "Decimal" - periodRegex.matches(cleanedValue) -> "Period" - rangeRegex.matches(cleanedValue) -> "Range" - moneyRegex.matches(cleanedValue) -> "Money" - durationRegex.matches(cleanedValue) -> "Duration" - ratioRegex.matches(cleanedValue) -> "Ratio" - quantityRegex.matches(cleanedValue) -> "Quantity" - humanNameRegex.matches(cleanedValue) -> "HumanName" - contactPointRegex.matches(cleanedValue) -> "ContactPoint" - base64BinaryRegex.matches(cleanedValue) -> "Base64Binary" - annotationRegex.matches(cleanedValue) -> "Annotation" - addressRegex.matches(cleanedValue) -> "Address" - identifierRegex.matches(cleanedValue) -> "Identifier" - codingRegex.matches(cleanedValue) -> "Coding" - narrativeRegex.matches(cleanedValue) -> "Narrative" - else -> "String" +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(): StringBuilder { - append(System.lineSeparator()) - return this + append(System.lineSeparator()) + return this } - private val Field.isList: Boolean - get() = isParameterized && type == List::class.java + get() = isParameterized && type == List::class.java private val Field.isParameterized: Boolean - get() = genericType is ParameterizedType + get() = genericType is ParameterizedType /** The non-parameterized type of this field (e.g. `String` for a field of type `List`). */ private val Field.nonParameterizedType: Class<*> - get() = - if (isParameterized) (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> - else type + get() = + if (isParameterized) { + (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + } else { + type + } private fun Class<*>.getFieldOrNull(name: String): Field? { - return try { - getDeclaredField(name) - } catch (ex: NoSuchFieldException) { - superclass?.getFieldOrNull(name) - } + return try { + getDeclaredField(name) + } catch (ex: NoSuchFieldException) { + superclass?.getFieldOrNull(name) + } } private fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { - val answerType = getType(questionnaireResponse) - return if (answerType != null) { - answerType == "org.hl7.fhir.r4.model.Coding" - } else { - false - } + val answerType = getType(questionnaireResponse) + return if (answerType != null) { + answerType == "org.hl7.fhir.r4.model.Coding" + } else { + false + } } private fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { - val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) - return answer.firstOrNull()?.javaClass?.name + return answer.firstOrNull()?.javaClass?.name } - internal val fhirPathEngine: FHIRPathEngine = - with(FhirContext.forCached(FhirVersionEnum.R4)) { - FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { - hostServices = FHIRPathEngineHostServices - } + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + hostServices = FHIRPathEngineHostServices } + } private fun String.isEnumeration(instruction: Instruction): Boolean { - return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false + return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false } - fun String.getAnswerType(questionnaireResponse: QuestionnaireResponse): String? { - return if (isEvaluateExpression()) { - val fhirPath = substring(indexOf(",") + 1, length - 1) - - fhirPath.getType(questionnaireResponse) - ?.replace("org.hl7.fhir.r4.model.", "") - } else { - // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual one that is generated and confirm if we need more conversions - "StringType"; - } + return if (isEvaluateExpression()) { + val fhirPath = substring(indexOf(",") + 1, length - 1) + + fhirPath.getType(questionnaireResponse)?.replace("org.hl7.fhir.r4.model.", "") + } else { + // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual + // one that is generated and confirm if we need more conversions + "StringType" + } } // TODO: Confirm and fix this fun String.isEvaluateExpression(): Boolean = startsWith("evaluate(") - /** * Infer's the type and return the short class name eg `HumanName` for org.fhir.hl7.r4.model.Patient * when given the path `Patient.name` */ fun inferType(propertyPath: String): String? { - // TODO: Handle possible errors - // TODO: Handle inferring nested types - val parts = propertyPath.split(".") - val parentResourceClassName = parts[0] - lateinit var parentClass: Class<*> - - if (fhirResources.contains(parentResourceClassName)) { - parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") - return inferType(parentClass, parts, 1) - } else { - return null - } + // TODO: Handle possible errors + // TODO: Handle inferring nested types + val parts = propertyPath.split(".") + val parentResourceClassName = parts[0] + lateinit var parentClass: Class<*> + + if (fhirResources.contains(parentResourceClassName)) { + parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") + return inferType(parentClass, parts, 1) + } else { + return null + } } fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? { - val resourcePropertyName = parts[index] - val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) - - val propertyType = if (propertyField?.isList == true) - propertyField.nonParameterizedType - // TODO: Check if this is required - else if (propertyField?.type == Enumeration::class.java) - // TODO: Check if this works - propertyField.nonParameterizedType - else - propertyField?.type - - return if (parts.size > index + 1) { - return inferType(propertyType, parts, index + 1) - } else - propertyType?.name - ?.replace("org.hl7.fhir.r4.model.", "") + val resourcePropertyName = parts[index] + val propertyField = parentClass?.getFieldOrNull(resourcePropertyName) + + val propertyType = + if (propertyField?.isList == true) { + propertyField.nonParameterizedType + } // TODO: Check if this is required + else if (propertyField?.type == Enumeration::class.java) { + // TODO: Check if this works + propertyField.nonParameterizedType + } else { + propertyField?.type + } + + return if (parts.size > index + 1) { + return inferType(propertyType, parts, index + 1) + } else { + propertyType?.name?.replace("org.hl7.fhir.r4.model.", "") + } } fun String.isMultipleTypes(): Boolean = this == "Type" -// Assuming a mock annotation to simulate the @Child.type annotation in FHIR -annotation class Child(val type: KClass) +// TODO: Finish this. Use the annotation @Chid.type fun String.getPossibleTypes(): List { - val clazz = Class.forName("org.hl7.fhir.r4.model.$this") - val possibleTypes = mutableListOf() - - clazz.declaredFields.forEach { field -> - val annotation = field.annotations.find { it is Child } as? Child - annotation?.let { - val typeInstance = it.type.java.getDeclaredConstructor().newInstance() - possibleTypes.add(typeInstance) - } - } - - return possibleTypes + 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", - "DateTimeType" to "StringType", - "TimeType" to "StringType", - "InstantType" to "DateTimeType", - "UriType" to "StringType", - "UuidType" to "StringType", - "CodeType" to "StringType", - "MarkdownType" to "StringType", - "Base64BinaryType" to "StringType", - "OidType" to "StringType", - "PositiveIntType" to "IntegerType", - "UnsignedIntType" to "IntegerType", - "IdType" to "StringType", - "CanonicalType" to "StringType" + 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 } + } - // Check if the source type can be converted to any of the possible types for this target type - val possibleTypes = this.getPossibleTypes() - possibleTypes.forEach { possibleType -> - if (possibleType::class.simpleName == sourceType) { - return true - } - } - - try { - propertyClass.getDeclaredMethod("fromCode", targetType2) - } catch (ex: NoSuchMethodException) { - return false - } + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } - return true + return true } - fun String.getParentResource(): String? { - return substring(0, lastIndexOf('.')) + return substring(0, lastIndexOf('.')) } - fun String.getResourceProperty(): String? { - return substring(lastIndexOf('.') + 1) + return substring(lastIndexOf('.') + 1) } -fun String.getFhirType(): String = replace("Type", "") - .lowercase() \ No newline at end of file +fun String.getFhirType(): String = replace("Type", "").lowercase() diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt index 6f3a3da7..c1146e87 100644 --- a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/FhirPathEngineHostServicesTest.kt @@ -1,80 +1,96 @@ package org.smartregister.fhir.structuremaptool import org.hl7.fhir.r4.model.StringType -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -class FHIRPathEngineHostServicesTest { +class FhirPathEngineHostServicesTest { - @Test - fun testResolveConstant() { - val appContext = mapOf("test" to StringType("Test Value")) - val result = FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + @Test + fun testResolveConstant() { + val appContext = mapOf("test" to StringType("Test Value")) + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - assertNotNull(result) - assertEquals("Test Value", (result as StringType).value) - } + assertNotNull(result) + assertEquals("Test Value", (result as StringType).value) + } - @Test - fun testResolveConstant_cache() { - // Set up the application context with a constant value - val appContext = mapOf("test" to StringType("Test Value")) + @Test + fun testResolveConstant_cache() { + // Set up the application context with a constant value + val appContext = mapOf("test" to StringType("Test Value")) - // First call: Resolves and caches the constant value - FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + // First call: Resolves and caches the constant value + FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - // Second call: Should retrieve the value from the cache - val result = FHIRPathEngineHostServices.resolveConstant(appContext, "test", false) + // Second call: Should retrieve the value from the cache + val result = FhirPathEngineHostServices.resolveConstant(appContext, "test", false) - // Verify that the result is not null and matches the expected value - assertNotNull(result, "The resolved constant should not be null") - assertEquals("Test Value", (result as StringType).value, "The resolved constant should match the expected value") - } + // Verify that the result is not null and matches the expected value + assertNotNull(result, "The resolved constant should not be null") + assertEquals( + "Test Value", + (result as StringType).value, + "The resolved constant should match the expected value" + ) + } + @Test + fun testLog() { + val logResult = FhirPathEngineHostServices.log("Test log message", mutableListOf()) + assertTrue(logResult) + } - @Test - fun testLog() { - val logResult = FHIRPathEngineHostServices.log("Test log message", mutableListOf()) - assertTrue(logResult) - } + @Test + fun testResolveFunction_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveFunction("testFunction") + } + assertEquals("Function testFunction is not yet implemented.", exception.message) + } - @Test - fun testResolveFunction_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveFunction("testFunction") - } - assertEquals("Function testFunction is not yet implemented.", exception.message) - } + @Test + fun testResolveValueSet_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveValueSet(null, "http://example.com") + } + assertEquals("resolveValueSet is not yet implemented.", exception.message) + } - @Test - fun testResolveValueSet_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveValueSet(null, "http://example.com") - } - assertEquals("resolveValueSet is not yet implemented.", exception.message) - } + @Test + fun testResolveReference_unsupported() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.resolveReference(null, "http://example.com") + } + assertEquals("resolveReference is not yet implemented.", exception.message) + } - @Test - fun testResolveReference_unsupported() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.resolveReference(null, "http://example.com") - } - assertEquals("resolveReference is not yet implemented.", exception.message) - } + @Test + fun testCheckFunctionThrowsUnsupportedOperationException() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) + } + assertEquals("checkFunction is not yet implemented.", exception.message) + } - @Test - fun testCheckFunctionThrowsUnsupportedOperationException() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.checkFunction(null, "testFunction", mutableListOf()) - } - assertEquals("checkFunction is not yet implemented.", exception.message) - } - - @Test - fun testExecuteFunctionThrowsUnsupportedException() { - val exception = assertThrows(UnsupportedOperationException::class.java) { - FHIRPathEngineHostServices.executeFunction(null, mutableListOf(), "testFunction", mutableListOf()) - } - assertEquals("executeFunction is not yet implemented.", exception.message) - } + @Test + fun testExecuteFunctionThrowsUnsupportedException() { + val exception = + assertThrows(UnsupportedOperationException::class.java) { + FhirPathEngineHostServices.executeFunction( + null, + mutableListOf(), + "testFunction", + mutableListOf() + ) + } + assertEquals("executeFunction is not yet implemented.", exception.message) + } } From 545de645c2d06f9e10c247d84386afaa817a92e7 Mon Sep 17 00:00:00 2001 From: Lentumunai Mark <90028422+Lentumunai-Mark@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:44:06 +0300 Subject: [PATCH 4/9] Test transform support services. (#282) Signed-off-by: Lentumunai-Mark Co-authored-by: Peter Lubell-Doughtie --- .../TransformSupportServices.kt | 38 ++--- .../TransformSupportServicesTest.kt | 137 ++++++++++++++++++ 2 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt diff --git a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt index e638a22b..36403e0a 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/TransformSupportServices.kt @@ -27,25 +27,25 @@ class TransformSupportServices constructor(val simpleWorkerContext: SimpleWorker println(message) } - @Throws(FHIRException::class) - override fun createType(appInfo: Any, name: String): Base { - return when (name) { - "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() - "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() - "Immunization_VaccinationProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() - "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() - "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() - "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() - "Encounter_Participant" -> Encounter.EncounterParticipantComponent() - "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() - "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() - "Patient_Link" -> Patient.PatientLinkComponent() - "Timing_Repeat" -> Timing.TimingRepeatComponent() - "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() - "Group_Characteristic" -> Group.GroupCharacteristicComponent() - "Observation_Component" -> Observation.ObservationComponentComponent() - else -> ResourceFactory.createResourceOrType(name) - } + @Throws(FHIRException::class) + override fun createType(appInfo: Any, name: String): Base { + return when (name) { + "RiskAssessment_Prediction" -> RiskAssessmentPredictionComponent() + "RiskAssessment\$RiskAssessmentPredictionComponent" -> RiskAssessmentPredictionComponent() + "Immunization_AppliedProtocol" -> Immunization.ImmunizationProtocolAppliedComponent() + "Immunization_Reaction" -> Immunization.ImmunizationReactionComponent() + "EpisodeOfCare_Diagnosis" -> EpisodeOfCare.DiagnosisComponent() + "Encounter_Diagnosis" -> Encounter.DiagnosisComponent() + "Encounter_Participant" -> Encounter.EncounterParticipantComponent() + "CarePlan_Activity" -> CarePlan.CarePlanActivityComponent() + "CarePlan_ActivityDetail" -> CarePlan.CarePlanActivityDetailComponent() + "Patient_Link" -> Patient.PatientLinkComponent() + "Timing_Repeat" -> Timing.TimingRepeatComponent() + "PlanDefinition_Action" -> PlanDefinition.PlanDefinitionActionComponent() + "Group_Characteristic" -> Group.GroupCharacteristicComponent() + "Observation_Component" -> Observation.ObservationComponentComponent() + else -> ResourceFactory.createResourceOrType(name) + } } override fun createResource(appInfo: Any, res: Base, atRootofTransform: Boolean): Base { diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt new file mode 100644 index 00000000..6d4f460f --- /dev/null +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt @@ -0,0 +1,137 @@ +package org.smartregister.fhir.structuremaptool + +import io.mockk.mockk +import org.hl7.fhir.exceptions.FHIRException +import org.hl7.fhir.r4.model.CarePlan +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.EpisodeOfCare +import org.hl7.fhir.r4.model.Immunization +import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.RiskAssessment +import org.hl7.fhir.r4.model.TimeType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import kotlin.test.Test + +class TransformSupportServicesTest{ + lateinit var transformSupportServices: TransformSupportServices + + @BeforeEach + fun setUp() { + transformSupportServices = TransformSupportServices(mockk()) + } + + + @Test + fun `createType() should return RiskAssessmentPrediction when given RiskAssessment_Prediction`() { + assertTrue( + transformSupportServices.createType("", "RiskAssessment_Prediction") + is RiskAssessment.RiskAssessmentPredictionComponent, + ) + } + + @Test + fun `createType() should return ImmunizationProtocol when given Immunization_VaccinationProtocol`() { + assertTrue( + transformSupportServices.createType("", "Immunization_AppliedProtocol") + is Immunization.ImmunizationProtocolAppliedComponent, + ) + } + + @Test + fun `createType() should return ImmunizationReaction when given Immunization_Reaction`() { + assertTrue( + transformSupportServices.createType("", "Immunization_Reaction") + is Immunization.ImmunizationReactionComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given EpisodeOfCare_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "EpisodeOfCare_Diagnosis") + is EpisodeOfCare.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return Diagnosis when given Encounter_Diagnosis`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Diagnosis") + is Encounter.DiagnosisComponent, + ) + } + + @Test + fun `createType() should return EncounterParticipant when given Encounter_Participant`() { + assertTrue( + transformSupportServices.createType("", "Encounter_Participant") + is Encounter.EncounterParticipantComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivity when given CarePlan_Activity`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_Activity") + is CarePlan.CarePlanActivityComponent, + ) + } + + @Test + fun `createType() should return CarePlanActivityDetail when given CarePlan_ActivityDetail`() { + assertTrue( + transformSupportServices.createType("", "CarePlan_ActivityDetail") + is CarePlan.CarePlanActivityDetailComponent, + ) + } + + @Test + fun `createType() should return PatientLink when given Patient_Link`() { + assertTrue( + transformSupportServices.createType("", "Patient_Link") is Patient.PatientLinkComponent, + ) + } + + @Test + fun `createType() should return ObservationComponentComponent when given Observation_Component`() { + assertTrue( + transformSupportServices.createType("", "Observation_Component") + is Observation.ObservationComponentComponent, + ) + } + + @Test + fun `createType() should return Time when given time`() { + assertTrue(transformSupportServices.createType("", "time") is TimeType) + } + + @Test + fun `createResource() should add resource into output when given Patient and atRootOfTransForm as True`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), true) + assertEquals(transformSupportServices.outputs.size, 1) + } + + @Test + fun `createResource() should not add resource into output when given Patient and atRootOfTransForm as False`() { + assertEquals(transformSupportServices.outputs.size, 0) + transformSupportServices.createResource("", Patient(), false) + assertEquals(transformSupportServices.outputs.size, 0) + } + + @Test + fun `resolveReference should throw FHIRException when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.resolveReference("", "https://url.com") + } + } + + @Test + fun `performSearch() should throw FHIRException this is not supported yet when given url`() { + assertThrows(FHIRException::class.java) { + transformSupportServices.performSearch("", "https://url.com") + } + } +} \ No newline at end of file From e5de4d6ac4c2fe16381c37b16908ead1c127ad28 Mon Sep 17 00:00:00 2001 From: Benjamin Mwalimu Date: Fri, 13 Sep 2024 17:56:28 +0300 Subject: [PATCH 5/9] -Add new OpenSRP web roles (#286) --- importer/csv/setup/roles.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/importer/csv/setup/roles.csv b/importer/csv/setup/roles.csv index aff12ebd..6a33ca39 100644 --- a/importer/csv/setup/roles.csv +++ b/importer/csv/setup/roles.csv @@ -118,3 +118,5 @@ WEB_CLIENT,, ANDROID_CLIENT,, EDIT_KEYCLOAK_USERS,TRUE,manage-users|query-users VIEW_KEYCLOAK_USERS,TRUE,view-users|query-users|query-groups +VIEW_USER_GROUPS,, +VIEW_ROLES,, \ No newline at end of file From 2689ee042a492be080013e7817eddbce4729d656 Mon Sep 17 00:00:00 2001 From: Benjamin Mwalimu Date: Tue, 17 Sep 2024 16:24:58 +0300 Subject: [PATCH 6/9] Update README.md (#287) --- importer/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importer/README.md b/importer/README.md index e9b9a83a..970239c3 100644 --- a/importer/README.md +++ b/importer/README.md @@ -1,6 +1,6 @@ # Setup Keycloak Roles -This script is used to setup keycloak roles and groups. It takes in a csv file with the following columns: +This script is used to set up keycloak roles and groups. It takes in a CSV file with the following columns: - **role**: The actual names of the roles you would like to create - **composite**: A boolean value that tells if the role has composite roles or not @@ -8,19 +8,19 @@ This script is used to setup keycloak roles and groups. It takes in a csv file w ### Options -- `setup` : (Required) This needs to be set to "roles" in order to initiate the setup process -- `csv_file` : (Required) The csv file with the list of roles +- `setup` : (Required) This needs to be set to "roles" to initiate the setup process +- `csv_file` : (Required) The CSV file with the list of roles - `group` : (Not required) This is the actual group name. If not passed then the roles will just be created but not assigned to any group -- `roles_max` : (Not required) This is the maximum number of roles to pull from the api. The default is set to 500. If the number of roles in your setup is more than this you will need to change this value +- `roles_max` : (Not required) This is the maximum number of roles to pull from the API. The default is set to 500. If the number of roles in your setup is more than this you will need to change this value - `default_groups` : (Not Required) This is a boolean value to turn on and off the assignment of default roles. The default value is `true` ### To run script 1. Create virtualenv 2. Install requirements.txt - `pip install -r requirements.txt` -3. Set up your _.env_ file, see sample below. Populate it with the right credentials, you can either provide an access token or client credentials. Ensure that the user whose details you provide in this _.env_ file has the necessary permissions/privileges. +3. Set up your _.env_ file, see the sample below. Populate it with the right credentials, you can either provide an access token or client credentials. Ensure the user whose details you provide in this _.env_ file has the necessary permissions/privileges. 4. Run script - `python3 main.py --setup roles --csv_file csv/setup/roles.csv --group Supervisor` -5. If you are running the script without `https` setup e.g locally or a server without https setup, you will need to set the `OAUTHLIB_INSECURE_TRANSPORT` environment variable to 1. For example `export OAUTHLIB_INSECURE_TRANSPORT=1 && python3 main.py --setup roles --csv_file csv/setup/roles.csv --group OpenSRP_Provider --log_level debug` +5. If you are running the script without `https` setup e.g. locally or a server without https setup, you will need to set the `OAUTHLIB_INSECURE_TRANSPORT` environment variable to 1. For example `export OAUTHLIB_INSECURE_TRANSPORT=1 && python3 main.py --setup roles --csv_file csv/setup/roles.csv --group OpenSRP_Provider --log_level debug` 6. You can turn on logging by passing a `--log_level` to the command line as `info`, `debug` or `error`. For example `python3 main.py --setup roles --csv_file csv/setup/roles.csv --group Supervisor --log_level debug` @@ -30,6 +30,7 @@ client_id = 'example-client-id' client_secret = 'example-client-secret' fhir_base_url = 'https://example.smartregister.org/fhir' keycloak_url = 'https://keycloak.smartregister.org/auth' +realm = 'example-realm' # access token for access to where product images are remotely stored product_access_token = 'example-product-access-token' From 87eff49fe1f57f1b54bca4b5655efbf591cccc90 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Fri, 23 Aug 2024 12:17:28 +0300 Subject: [PATCH 7/9] Enhance determineFhirDataType function --- .../Main.kt | 562 ++++++----- .../Utils.kt | 930 ++++++++++-------- 2 files changed, 821 insertions(+), 671 deletions(-) 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..b200248f 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Main.kt @@ -6,25 +6,26 @@ import ca.uhn.fhir.parser.IParser import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.prompt -import java.io.File -import java.io.FileInputStream -import java.nio.charset.Charset -import java.util.* import org.apache.commons.io.FileUtils import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.Row +import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory import org.hl7.fhir.r4.context.SimpleWorkerContext +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse -import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager import org.hl7.fhir.utilities.npm.ToolsVersion +import java.io.File +import java.io.FileInputStream +import java.nio.charset.Charset +import java.util.* fun main(args: Array) { - Application().main(args) + Application().main(args) } /*fun main(args: Array) { @@ -36,6 +37,7 @@ fun main(args: Array) { }*/ + /* REMAINING TASKS @@ -46,297 +48,353 @@ REMAINING TASKS */ class Application : CliktCommand() { - val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") - val questionnairefile: String by - option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") - - override fun run() { - // Create a map of Resource -> questionnaire name or path -> value - // For each resource loop through creating or adding the correct instructions - - lateinit var questionnaireResponse: QuestionnaireResponse - val contextR4 = FhirContext.forR4() - val fhirJsonParser = contextR4.newJsonParser() - val questionnaire: Questionnaire = - fhirJsonParser.parseResource( - Questionnaire::class.java, - FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) - ) - val questionnaireResponseFile = - File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) - if (questionnaireResponseFile.exists()) { - questionnaireResponse = - fhirJsonParser.parseResource( - QuestionnaireResponse::class.java, - questionnaireResponseFile.readText(Charset.defaultCharset()) - ) - } else { - println("File not found: questionnaire-response.json") - } + val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") + val questionnairefile : String by option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") + + + override fun run() { + // Create a map of Resource -> questionnaire name or path -> value + // For each resource loop through creating or adding the correct instructions + + lateinit var questionnaireResponse: QuestionnaireResponse + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire : Questionnaire = fhirJsonParser.parseResource(Questionnaire::class.java, FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset())) + val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) + if (questionnaireResponseFile.exists()) { + questionnaireResponse = fhirJsonParser.parseResource(QuestionnaireResponse::class.java, questionnaireResponseFile.readText(Charset.defaultCharset())) + } else { + println("File not found: questionnaire-response.json") + } - // reads the xls - val xlsFile = FileInputStream(xlsfile) - val xlWb = WorkbookFactory.create(xlsFile) + // reads the xls + val xlsFile = FileInputStream(xlsfile) + val xlWb = WorkbookFactory.create(xlsFile) - // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. - // RiskFlag in the previous XLSX was not valid - // TODO: Check that all the path's and other entries in the excel sheet are valid - // TODO: Add instructions for adding embedded classes like - // `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices + // Validate resources and paths in the XLS sheet + validateResourcesAndPaths(xlWb) - /* + // Fix groups calling sequence + fixGroupCallingSequence(xlWb) + // TODO: Check that all the Resource(s) ub the Resource column are the correct name and type eg. RiskFlag in the previous XLSX was not valid + // TODO: Check that all the path's and other entries in the excel sheet are valid + // TODO: Add instructions for adding embedded classes like `RiskAssessment$RiskAssessmentPredictionComponent` to the TransformSupportServices - READ THE SETTINGS SHEET + /* - */ - val settingsWorkbook = xlWb.getSheet("Settings") - var questionnaireId: String? = null + READ THE SETTINGS SHEET - for (i in 0..settingsWorkbook.lastRowNum) { - val cell = settingsWorkbook.getRow(i).getCell(0) + */ + val settingsWorkbook = xlWb.getSheet("Settings") + var questionnaireId : String? = null - if (cell.stringCellValue == "questionnaire-id") { - questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue - } - } + for (i in 0..settingsWorkbook.lastRowNum) { + val cell = settingsWorkbook.getRow(i).getCell(0) + + if (cell.stringCellValue == "questionnaire-id") { + questionnaireId = settingsWorkbook.getRow(i).getCell(1).stringCellValue + } + } - /* + /* - END OF READ SETTINGS SHEET + END OF READ SETTINGS SHEET - */ + */ - /* - TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first - We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! - */ - val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } - if (questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()) { - val sb = StringBuilder() - val structureMapHeader = - """ + /* + TODO: Fix Groups calling sequence so that Groups that depend on other resources to be generated need to be called first + We can also throw an exception if to figure out cyclic dependency. Good candidate for Floyd's tortoise and/or topological sorting 😁. Cool!!!! + */ + val questionnaireResponseItemIds = questionnaireResponse.item.map { it.id } + if(questionnaireId != null && questionnaireResponseItemIds.isNotEmpty()){ + + val sb = StringBuilder() + val structureMapHeader = """ map "http://hl7.org/fhir/StructureMap/$questionnaireId" = '${questionnaireId.clean()}' uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireReponse" as source uses "http://hl7.org/fhir/StructureDefinition/Bundle" as target - """ - .trimIndent() + """.trimIndent() - val structureMapBody = - """ + val structureMapBody = """ group ${questionnaireId.clean()}(source src : QuestionnaireResponse, target bundle: Bundle) { src -> bundle.id = uuid() "rule_c"; src -> bundle.type = 'collection' "rule_b"; - src -> bundle.entry as entry then - """ - .trimIndent() - - val lineNos = 1 - var firstResource = true - val extractionResources = hashMapOf() - val resourceConversionInstructions = hashMapOf>() - - // Group the rules according to the resource - val fieldMappingsSheet = xlWb.getSheet("Field Mappings") - fieldMappingsSheet.forEachIndexed { index, row -> - if (index == 0) return@forEachIndexed - - if (row.isEmpty()) { - return@forEachIndexed + src -> bundle.entry as entry then """.trimIndent() + + /* + + Create a mapping of COLUMN_NAMES to COLUMN indexes + + */ + //val mapColumns + + + val lineNos = 1 + var firstResource = true + val extractionResources = hashMapOf() + val resourceConversionInstructions = hashMapOf>() + + // Group the rules according to the resource + val fieldMappingsSheet = xlWb.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + + if (row.isEmpty()) { + return@forEachIndexed + } + + + val instruction = row.getInstruction() + val xlsId = instruction.responseFieldId + val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) + if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { + resourceConversionInstructions.computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) + .add(instruction) + } + } + //val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as Resource + + + // Perform the extraction for the row + /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) + + extractionResources[resourceName + resourceIndex] = resource*/ + + sb.append(structureMapHeader) + sb.appendNewLine().appendNewLine().appendNewLine() + sb.append(structureMapBody) + + // Fix the questions path + val questionsPath = getQuestionsPath(questionnaire) + + // TODO: Generate the links to the group names here + var index = 0 + var len = resourceConversionInstructions.size + var resourceName = "" + resourceConversionInstructions.forEach { entry -> + resourceName = entry.key.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + if (index++ != 0) sb.append(",") + if(resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") + } + sb.append(""" "rule_a";""".trimMargin()) + sb.appendNewLine() + sb.append("}") + + // Add the embedded instructions + val groupNames = mutableListOf() + + sb.appendNewLine().appendNewLine().appendNewLine() + + resourceConversionInstructions.forEach { + Group(it, sb, questionsPath) + .generateGroup(questionnaireResponse) + } + + val structureMapString = sb.toString() + try { + val simpleWorkerContext = SimpleWorkerContext().apply { + setExpansionProfile(Parameters()) + isCanRunWithoutTerminology = true + } + val transformSupportServices = TransformSupportServices(simpleWorkerContext) + val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) + val structureMap = scu.parse(structureMapString, questionnaireId.clean()) + // DataFormatException | FHIRLexerException + + try{ + val bundle = Bundle() + scu.transform(contextR4, questionnaireResponse, structureMap, bundle) + val jsonParser = FhirContext.forR4().newJsonParser() + + println(jsonParser.encodeResourceToString(bundle)) + } catch (e:Exception){ + e.printStackTrace() + } + + } catch (ex: Exception) { + println("The generated StructureMap has a formatting error") + ex.printStackTrace() + } + + var finalStructureMap = sb.toString() + finalStructureMap = finalStructureMap.addIdentation() + println(finalStructureMap) + writeStructureMapOutput(sb.toString().addIdentation()) + } + + } + private fun validateResourcesAndPaths(workbook: Workbook) { + val fieldMappingsSheet = workbook.getSheet("Field Mappings") + fieldMappingsSheet.forEachIndexed { index, row -> + if (index == 0) return@forEachIndexed + + val resourceName = row.getCellAsString(2) + val fieldPath = row.getCellAsString(4) + + if (!isValidResource(resourceName)) { + throw IllegalArgumentException("Invalid resource name: $resourceName") + } + + if (!isValidPath(fieldPath)) { + throw IllegalArgumentException("Invalid field path: $fieldPath") + } } + } + private fun isValidResource(resourceName: String?): Boolean { + // Implement logic to validate resource names + // This can be a list of known valid resource names, or a more complex validation + return resourceName != null && resourceName.isNotEmpty() + } + + private fun isValidPath(path: String?): Boolean { + // Implement logic to validate paths + // This can involve checking against known paths or ensuring the format is correct + return path != null && path.isNotEmpty() + } + + private fun fixGroupCallingSequence(workbook: Workbook) { + // Implement logic to fix group calling sequences + // Detect and handle cyclic dependencies, using topological sorting or other methods + // You can throw an exception if a cyclic dependency is detected + } + + private fun groupRulesByResource(workbook: Workbook, questionnaireResponseItemIds: List): Map> { + val fieldMappingsSheet = workbook.getSheet("Field Mappings") + val resourceConversionInstructions = hashMapOf>() + + 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 instruction = row.getInstruction() + val xlsId = instruction.responseFieldId + val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) + if (instruction.resource.isNotEmpty() && comparedResponseAndXlsId) { + resourceConversionInstructions.computeIfAbsent(instruction.searchKey(), { key -> mutableListOf() }) + .add(instruction) + } } - } - // val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as - // Resource - - // Perform the extraction for the row - /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) - - extractionResources[resourceName + resourceIndex] = resource*/ - - sb.append(structureMapHeader) - sb.appendNewLine().appendNewLine().appendNewLine() - sb.append(structureMapBody) - - // Fix the questions path - val questionsPath = getQuestionsPath(questionnaire) - - // TODO: Generate the links to the group names here - var index = 0 - var len = resourceConversionInstructions.size - var resourceName = "" - resourceConversionInstructions.forEach { entry -> - resourceName = - entry.key.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() - } - if (index++ != 0) sb.append(",") - if (resourceName.isNotEmpty()) sb.append("Extract$resourceName(src, bundle)") - } - sb.append(""" "rule_a";""".trimMargin()) - sb.appendNewLine() - sb.append("}") - - // Add the embedded instructions - val groupNames = mutableListOf() - - sb.appendNewLine().appendNewLine().appendNewLine() - - resourceConversionInstructions.forEach { - Group(it, sb, questionsPath).generateGroup(questionnaireResponse) - } - - val structureMapString = sb.toString() - try { - val simpleWorkerContext = - SimpleWorkerContext().apply { - setExpansionProfile(Parameters()) - isCanRunWithoutTerminology = true - } - val transformSupportServices = TransformSupportServices(simpleWorkerContext) - val scu = - org.hl7.fhir.r4.utils.StructureMapUtilities(simpleWorkerContext, transformSupportServices) - val structureMap = scu.parse(structureMapString, questionnaireId.clean()) - // DataFormatException | FHIRLexerException - - try { - val bundle = Bundle() - scu.transform(contextR4, questionnaireResponse, structureMap, bundle) - val jsonParser = FhirContext.forR4().newJsonParser() - - println(jsonParser.encodeResourceToString(bundle)) - } catch (e: Exception) { - e.printStackTrace() + + return resourceConversionInstructions + } + + 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 } - } catch (ex: Exception) { - println("The generated StructureMap has a formatting error") - ex.printStackTrace() - } - - var finalStructureMap = sb.toString() - finalStructureMap = finalStructureMap.addIndentation() - println(finalStructureMap) - writeStructureMapOutput(sb.toString().addIndentation()) } - } - - fun Row.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.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.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 String.clean() : String { + return this.replace("-", "") + .replace("_", "") + .replace(" ", "") + } - fun String.clean(): String { - return this.replace("-", "").replace("_", "").replace(" ", "") - } } class Instruction { - var responseFieldId: String? = null - var constantValue: String? = null - var resource: String = "" - var resourceIndex: Int = 0 - var fieldPath: String = "" - var field: String? = null - var conversion: String? = null - var fhirPathStructureMapFunctions: String? = null + var responseFieldId : String? = null + var constantValue: String? = null + var resource: String = "" + var resourceIndex: Int = 0 + var fieldPath: String = "" + var field: String? = null + var conversion : String? = null + var fhirPathStructureMapFunctions: String? = null - // TODO: Clean the following properties - var fullFieldPath = "" - fun fullPropertyPath(): String = "$resource.$fullFieldPath" + // TODO: Clean the following properties + var fullFieldPath = "" + fun fullPropertyPath() : String = "$resource.$fullFieldPath" - fun searchKey() = resource + resourceIndex + fun searchKey() = resource + resourceIndex } fun Instruction.copyFrom(instruction: Instruction) { - constantValue = instruction.constantValue - resource = instruction.resource - resourceIndex = instruction.resourceIndex - fieldPath = instruction.fieldPath - fullFieldPath = instruction.fullFieldPath - field = instruction.field - conversion = instruction.conversion - fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions + constantValue = instruction.constantValue + resource = instruction.resource + resourceIndex = instruction.resourceIndex + fieldPath = instruction.fieldPath + fullFieldPath = instruction.fullFieldPath + field = instruction.field + conversion = instruction.conversion + fhirPathStructureMapFunctions = instruction.fhirPathStructureMapFunctions } -fun String.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() + +fun String.addIdentation() : String { + var currLevel = 0 + + val lines = split("\n") + + val sb = StringBuilder() + lines.forEach { line -> + if (line.endsWith("{")) { + sb.append(line.addIdentation(currLevel)) + sb.appendNewLine() + currLevel++ + } else if (line.startsWith("}")) { + currLevel-- + sb.append(line.addIdentation(currLevel)) + sb.appendNewLine() + } else { + sb.append(line.addIdentation(currLevel)) + sb.appendNewLine() + } } - } - return sb.toString() + return sb.toString() } -fun String.addIndentation(times: Int): String { - var processedString = "" - for (k in 1..times) { - processedString += "\t" - } +fun String.addIdentation(times: Int) : String { + var processedString = "" + for (k in 1..times) { + processedString += "\t" + } - processedString += this - return processedString + processedString += this + return processedString } -fun writeStructureMapOutput(structureMap: String) { - File("generated-structure-map.txt").writeText(structureMap.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.addIdentation()) + val pcm = FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION) + val contextR5 = SimpleWorkerContext.fromPackage(pcm.loadPackage("hl7.fhir.r4.core", "4.0.1")) + contextR5.setExpansionProfile(Parameters()) + contextR5.isCanRunWithoutTerminology = true + val transformSupportServices = TransformSupportServices(contextR5) + val scu = org.hl7.fhir.r4.utils.StructureMapUtilities(contextR5, transformSupportServices) + val map = scu.parse(structureMap, "LocationRegistration") + val iParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) + val mapString = iParser.encodeResourceToString(map) + File("generated-json-map.json").writeText(mapString) } 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..d214a9e7 100644 --- a/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt +++ b/sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt @@ -2,8 +2,6 @@ package org.smartregister.fhir.structuremaptool import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum -import java.lang.reflect.Field -import java.lang.reflect.ParameterizedType import org.apache.poi.ss.usermodel.Row import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Enumeration @@ -13,534 +11,628 @@ 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 kotlin.reflect.KClass // 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() - } - - 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'" - } + 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() } - // 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 "''" + 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") } - 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 + fun addRuleNo() { + stringBuilder.append(""" "${groupName}_${lineCounter++}"; """) } - /* - 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 - } + fun Instruction.getPropertyPath(): String { + return questionsPath.getOrDefault(responseFieldId, "") + } - if (index > 0 && index < parts.size - 1) { - newFieldPath += "." - } - } + fun Instruction.getAnswerExpression(questionnaireResponse: QuestionnaireResponse): String { - fieldPath = newFieldPath + //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'" + } + } - it.add(nextInstruction) + // 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 + } - return@run + // 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 + } - // If no match is found, let's create a new one - val newNest = - Nest().apply { - name = partName + /* + 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 + */ - 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 - } + 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 + } } - fieldPath = newFieldPath - } - add(nextInstruction) - } else { - this@apply.instruction = instruction - } + // 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") ?: "" + }) + } } - 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 - } + 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) - 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 {") - } + 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() + if (!name.equals("")) { + stringBuilder.append("}") + addRuleNo() + } else { + //addRuleNo() + } + stringBuilder.appendNewLine() + } else { + throw Exception("nest & instruction are null inside Nest object") + } } - 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 fieldPath = row.getCell(4)?.stringCellValue ?: "" + val cellValue = row.getCell(0)?.stringCellValue ?: "" + + // Determine the target FHIR data type val targetDataType = determineFhirDataType(cellValue) - structureMapBody.append("src -> entity.$fieldPath=") + // Generate the mapping line for the StructureMap + structureMapBody.append("src -> entity.$fieldPath = ") + + // Handle different data types 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'") - } + "string" -> { + structureMapBody.append("create('string').value = '${cellValue.escapeQuotes()}'") + } + + "integer" -> { + structureMapBody.append("create('integer').value = ${cellValue.toIntOrNull() ?: 0}") + } + + "boolean" -> { + val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" + structureMapBody.append("create('boolean').value = $booleanValue") + } + + "date" -> { + // Handle date type + structureMapBody.append("create('date').value = '${cellValue.escapeQuotes()}'") + } + + "decimal" -> { + // Handle decimal type + structureMapBody.append("create('decimal').value = ${cellValue.toDoubleOrNull() ?: 0.0}") + } + + "code" -> { + // Handle code type + structureMapBody.append("create('code').value = '${cellValue.escapeQuotes()}'") + } + + else -> { + structureMapBody.append("create('unsupportedDataType').value = '${cellValue.escapeQuotes()}'") + } } 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 String.escapeQuotes(): String { + return this.replace("'", "\\'") +} + +fun determineFhirDataType(input: String?): String { + if (input.isNullOrEmpty()) { + return "Invalid Input: Null or Empty String" + } + + // Clean and prepare the input for matching + val cleanedValue = input.trim().toLowerCase() + + // Regular Expressions for FHIR Data Types + val booleanRegex = "^(true|false)\$".toRegex(RegexOption.IGNORE_CASE) + val integerRegex = "^-?\\d+\$".toRegex() + val decimalRegex = "^-?\\d*\\.\\d+\$".toRegex() + val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() // YYYY-MM-DD + val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() // YYYY-MM-DDThh:mm:ssZ + val stringRegex = "^[\\w\\s]+\$".toRegex() + val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() // e.g., "70 kg" + val codeableConceptRegex = "^[\\w\\s]+\$".toRegex() // Simplified for now + val codingRegex = "^\\w+\\|\$".toRegex() // Simplified for now + val referenceRegex = "^\\w+/\\w+\$".toRegex() // e.g., "Patient/123" + val periodRegex = "^\\d{4}-\\d{2}-\\d{2}\\/\\d{4}-\\d{2}-\\d{2}\$".toRegex() // e.g., "2023-01-01/2023-12-31" + val timingRegex = "^\\d+[a-zA-Z]+\$".toRegex() // Simplified for now + val rangeRegex = "^\\d+-\\d+\$".toRegex() // e.g., "10-20" + val annotationRegex = """^.*\s+\S+""".toRegex() // A basic regex for general text or comments + val attachmentRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 encoded string (could be more complex) + val base64BinaryRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Similar to attachment, but could have specific markers + val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // Regex for phone numbers (E.164 format) + val humanNameRegex = """^[A-Za-z\s'-]+$""".toRegex() // Simple regex for names + val addressRegex = """^\d+\s[A-Za-z]+\s[A-Za-z]+""".toRegex() // Basic address pattern + val durationRegex = """^\d+\s(hour|minute|second|day)$""".toRegex() // Duration like "1 hour" + val moneyRegex = """^\d+(\.\d{2})?\s[A-Z]{3}$""".toRegex() // Money format like "100.00 USD" + val ratioRegex = """^\d+:\d+$""".toRegex() // Simple ratio like "1:1000" + val signatureRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 signature + val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() // Identifier format + val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() // Simple URI format + val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() // UUID format + val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() // ISO 8601 instant format + val narrativeRegex = """.*<\/div>""".toRegex() // Narrative XHTML content + val sampledDataRegex = """^.*\s+\S+""".toRegex() // Placeholder regex for complex observation data + val backboneElementRegex = """^.*$""".toRegex() // Catch-all for complex structures (requires specific context) + // Detect and Return FHIR Data Type + return when { + booleanRegex.matches(cleanedValue) -> "Boolean" + integerRegex.matches(cleanedValue) -> "Integer" + decimalRegex.matches(cleanedValue) -> "Decimal" + dateRegex.matches(cleanedValue) -> "Date" + dateTimeRegex.matches(cleanedValue) -> "DateTime" + quantityRegex.matches(input) -> "Quantity" + codeableConceptRegex.matches(input) -> "CodeableConcept" + codingRegex.matches(input) -> "Coding" + identifierRegex.matches(input) -> "Identifier" + referenceRegex.matches(input) -> "Reference" + periodRegex.matches(input) -> "Period" + timingRegex.matches(input) -> "Timing" + rangeRegex.matches(input) -> "Range" + stringRegex.matches(input) -> "String" + annotationRegex.matches(input) -> "Annotation" + attachmentRegex.matches(input) -> "Attachment" + base64BinaryRegex.matches(input) -> "Base64Binary" + contactPointRegex.matches(input) -> "ContactPoint" + humanNameRegex.matches(input) -> "HumanName" + addressRegex.matches(input) -> "Address" + durationRegex.matches(input) -> "Duration" + moneyRegex.matches(input) -> "Money" + ratioRegex.matches(input) -> "Ratio" + signatureRegex.matches(input) -> "Signature" + identifierRegex.matches(input) -> "Identifier" + uriRegex.matches(input) -> "Uri" + uuidRegex.matches(input) -> "Uuid" + instantRegex.matches(input) -> "Instant" + narrativeRegex.matches(input) -> "Narrative" + sampledDataRegex.matches(input) -> "SampledData" + backboneElementRegex.matches(input) -> "BackboneElement" + else -> "String" } - } } + fun StringBuilder.appendNewLine(): StringBuilder { - append(System.lineSeparator()) - return this + append(System.lineSeparator()) + return this } + private val Field.isList: Boolean - get() = isParameterized && type == List::class.java + get() = isParameterized && type == List::class.java private val Field.isParameterized: Boolean - get() = genericType is ParameterizedType + get() = genericType is ParameterizedType /** The non-parameterized type of this field (e.g. `String` for a field of type `List`). */ private val Field.nonParameterizedType: Class<*> - get() = - if (isParameterized) { - (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> - } else { - type - } + get() = + if (isParameterized) (genericType as ParameterizedType).actualTypeArguments[0] as Class<*> + else type private fun Class<*>.getFieldOrNull(name: String): Field? { - return try { - getDeclaredField(name) - } catch (ex: NoSuchFieldException) { - superclass?.getFieldOrNull(name) - } + return try { + getDeclaredField(name) + } catch (ex: NoSuchFieldException) { + superclass?.getFieldOrNull(name) + } } private fun String.isCoding(questionnaireResponse: QuestionnaireResponse): Boolean { - val answerType = getType(questionnaireResponse) - return if (answerType != null) { - answerType == "org.hl7.fhir.r4.model.Coding" - } else { - false - } + val answerType = getType(questionnaireResponse) + return if (answerType != null) { + answerType == "org.hl7.fhir.r4.model.Coding" + } else { + false + } } private fun String.getType(questionnaireResponse: QuestionnaireResponse): String? { - val answer = fhirPathEngine.evaluate(questionnaireResponse, this) + val answer = fhirPathEngine.evaluate(questionnaireResponse, this) - return answer.firstOrNull()?.javaClass?.name + return answer.firstOrNull()?.javaClass?.name } + internal val fhirPathEngine: FHIRPathEngine = - with(FhirContext.forCached(FhirVersionEnum.R4)) { - FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { - hostServices = FHIRPathEngineHostServices + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)).apply { + hostServices = FHIRPathEngineHostServices + } } - } private fun String.isEnumeration(instruction: Instruction): Boolean { - return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false + return inferType(instruction.fullPropertyPath())?.contains("Enumeration") ?: false } + fun String.getAnswerType(questionnaireResponse: QuestionnaireResponse): String? { - return if (isEvaluateExpression()) { - val fhirPath = substring(indexOf(",") + 1, length - 1) - - fhirPath.getType(questionnaireResponse)?.replace("org.hl7.fhir.r4.model.", "") - } else { - // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual - // one that is generated and confirm if we need more conversions - "StringType" - } + return if (isEvaluateExpression()) { + val fhirPath = substring(indexOf(",") + 1, length - 1) + + fhirPath.getType(questionnaireResponse) + ?.replace("org.hl7.fhir.r4.model.", "") + } else { + // TODO: WE can run the actual line against StructureMapUtilities.runTransform to get the actual one that is generated and confirm if we need more conversions + "StringType"; + } } // TODO: Confirm and fix this fun String.isEvaluateExpression(): Boolean = startsWith("evaluate(") + /** * Infer's the type and return the short class name eg `HumanName` for org.fhir.hl7.r4.model.Patient * when given the path `Patient.name` */ fun inferType(propertyPath: String): String? { - // TODO: Handle possible errors - // TODO: Handle inferring nested types - val parts = propertyPath.split(".") - val parentResourceClassName = parts[0] - lateinit var parentClass: Class<*> - - if (fhirResources.contains(parentResourceClassName)) { - parentClass = Class.forName("org.hl7.fhir.r4.model.$parentResourceClassName") - return inferType(parentClass, parts, 1) - } else { - return null - } -} - -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 + // 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 { - propertyField?.type + return null } +} - return if (parts.size > index + 1) { - return inferType(propertyType, parts, index + 1) - } else { - propertyType?.name?.replace("org.hl7.fhir.r4.model.", "") - } +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 +// Assuming a mock annotation to simulate the @Child.type annotation in FHIR +annotation class Child(val type: KClass) fun String.getPossibleTypes(): List { - return listOf() + val clazz = Class.forName("org.hl7.fhir.r4.model.$this") + val possibleTypes = mutableListOf() + + clazz.declaredFields.forEach { field -> + val annotation = field.annotations.find { it is Child } as? Child + annotation?.let { + val typeInstance = it.type.java.getDeclaredConstructor().newInstance() + possibleTypes.add(typeInstance) + } + } + + return possibleTypes } + 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", + "DateTimeType" to "StringType", + "TimeType" to "StringType", + "InstantType" to "DateTimeType", + "UriType" to "StringType", + "UuidType" to "StringType", + "CodeType" to "StringType", + "MarkdownType" to "StringType", + "Base64BinaryType" to "StringType", + "OidType" to "StringType", + "PositiveIntType" to "IntegerType", + "UnsignedIntType" to "IntegerType", + "IdType" to "StringType", + "CanonicalType" to "StringType" ) - 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 - } + // Check if the source type can be converted to any of the possible types for this target type + val possibleTypes = this.getPossibleTypes() + possibleTypes.forEach { possibleType -> + if (possibleType::class.simpleName == sourceType) { + return true + } + } - return true + try { + propertyClass.getDeclaredMethod("fromCode", targetType2) + } catch (ex: NoSuchMethodException) { + return false + } + + 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() +fun String.getFhirType(): String = replace("Type", "") + .lowercase() \ No newline at end of file From beb1d93a8cbaa1c45eef1402e02bd0fc0fb14873 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Mon, 23 Sep 2024 10:01:31 +0300 Subject: [PATCH 8/9] Resolve conflicts --- .../kotlin/org.smartregister.fhir.structuremaptool/Utils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d214a9e7..d1ac8b6a 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 @@ -493,7 +493,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 } } From 05447908d69fecc30524f1456fe784d8475f8aa2 Mon Sep 17 00:00:00 2001 From: sharon2719 Date: Mon, 30 Sep 2024 10:44:00 +0300 Subject: [PATCH 9/9] Resolve conflicts --- .../Main.kt | 144 ++++--------- .../Utils.kt | 203 ++---------------- .../TransformSupportServicesTest.kt | 8 +- .../kotlin/utils/DetermineFhirDataTypeTest.kt | 2 +- 4 files changed, 55 insertions(+), 302 deletions(-) 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 94b49244..fd244ae6 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 @@ -13,7 +13,6 @@ import java.util.* import org.apache.commons.io.FileUtils import org.apache.poi.ss.usermodel.CellType import org.apache.poi.ss.usermodel.Row -import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.usermodel.WorkbookFactory import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Bundle @@ -38,37 +37,44 @@ REMAINING TASKS */ class Application : CliktCommand() { - val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") - val questionnairefile : String by option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") - - - override fun run() { - // Create a map of Resource -> questionnaire name or path -> value - // For each resource loop through creating or adding the correct instructions - - lateinit var questionnaireResponse: QuestionnaireResponse - val contextR4 = FhirContext.forR4() - val fhirJsonParser = contextR4.newJsonParser() - val questionnaire : Questionnaire = fhirJsonParser.parseResource(Questionnaire::class.java, FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset())) - val questionnaireResponseFile = File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) - if (questionnaireResponseFile.exists()) { - questionnaireResponse = fhirJsonParser.parseResource(QuestionnaireResponse::class.java, questionnaireResponseFile.readText(Charset.defaultCharset())) - } else { - println("File not found: questionnaire-response.json") - } + val xlsfile: String by option(help = "XLS filepath").prompt("Kindly enter the XLS filepath") + val questionnairefile: String by + option(help = "Questionnaire filepath").prompt("Kindly enter the questionnaire filepath") + + override fun run() { + // Create a map of Resource -> questionnaire name or path -> value + // For each resource loop through creating or adding the correct instructions + + lateinit var questionnaireResponse: QuestionnaireResponse + val contextR4 = FhirContext.forR4() + val fhirJsonParser = contextR4.newJsonParser() + val questionnaire: Questionnaire = + fhirJsonParser.parseResource( + Questionnaire::class.java, + FileUtils.readFileToString(File(questionnairefile), Charset.defaultCharset()) + ) + val questionnaireResponseFile = + File(javaClass.classLoader.getResource("questionnaire-response.json")?.file.toString()) + if (questionnaireResponseFile.exists()) { + questionnaireResponse = + fhirJsonParser.parseResource( + QuestionnaireResponse::class.java, + questionnaireResponseFile.readText(Charset.defaultCharset()) + ) + } else { + println("File not found: questionnaire-response.json") + } - // reads the xls - val xlsFile = FileInputStream(xlsfile) - val xlWb = WorkbookFactory.create(xlsFile) + // reads the xls + val xlsFile = FileInputStream(xlsfile) + val xlWb = WorkbookFactory.create(xlsFile) - // Validate resources and paths in the XLS sheet - validateResourcesAndPaths(xlWb) + // 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 - // Fix groups calling sequence - fixGroupCallingSequence(xlWb) - // 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 @@ -131,77 +137,6 @@ class Application : CliktCommand() { return@forEachIndexed } - } - private fun validateResourcesAndPaths(workbook: Workbook) { - val fieldMappingsSheet = workbook.getSheet("Field Mappings") - fieldMappingsSheet.forEachIndexed { index, row -> - if (index == 0) return@forEachIndexed - - val resourceName = row.getCellAsString(2) - val fieldPath = row.getCellAsString(4) - - if (!isValidResource(resourceName)) { - throw IllegalArgumentException("Invalid resource name: $resourceName") - } - - if (!isValidPath(fieldPath)) { - throw IllegalArgumentException("Invalid field path: $fieldPath") - } - } - } - private fun isValidResource(resourceName: String?): Boolean { - // Implement logic to validate resource names - // This can be a list of known valid resource names, or a more complex validation - return resourceName != null && resourceName.isNotEmpty() - } - - private fun isValidPath(path: String?): Boolean { - // Implement logic to validate paths - // This can involve checking against known paths or ensuring the format is correct - return path != null && path.isNotEmpty() - } - - private fun fixGroupCallingSequence(workbook: Workbook) { - // Implement logic to fix group calling sequences - // Detect and handle cyclic dependencies, using topological sorting or other methods - // You can throw an exception if a cyclic dependency is detected - } - - private fun groupRulesByResource(workbook: Workbook, questionnaireResponseItemIds: List): Map> { - val fieldMappingsSheet = workbook.getSheet("Field Mappings") - val resourceConversionInstructions = hashMapOf>() - - 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) - } - } - - return resourceConversionInstructions - } - - fun Row.getInstruction() : Instruction { - return Instruction().apply { - responseFieldId = getCell(0) ?.stringCellValue - constantValue = getCellAsString(1) - resource = getCell(2).stringCellValue - resourceIndex = getCell(3) ?.numericCellValue?.toInt() ?: 0 - fieldPath = getCell(4) ?.stringCellValue ?: "" - fullFieldPath = fieldPath - field = getCell(5) ?.stringCellValue - conversion = getCell(6) ?.stringCellValue - fhirPathStructureMapFunctions = getCell(7) ?.stringCellValue - val instruction = row.getInstruction() val xlsId = instruction.responseFieldId val comparedResponseAndXlsId = questionnaireResponseItemIds.contains(xlsId) @@ -211,13 +146,6 @@ class Application : CliktCommand() { .add(instruction) } } - // val resource = ?: Class.forName("org.hl7.fhir.r4.model.$resourceName").newInstance() as - // Resource - - // Perform the extraction for the row - /*generateStructureMapLine(structureMapBody, row, resource, extractionResources) - - extractionResources[resourceName + resourceIndex] = resource*/ sb.append(structureMapHeader) sb.appendNewLine().appendNewLine().appendNewLine() @@ -395,4 +323,4 @@ fun writeStructureMapOutput(structureMap: String) { FhirContext.forCached(FhirVersionEnum.R4).newJsonParser().setPrettyPrint(true) val mapString = iParser.encodeResourceToString(map) File("generated-json-map.json").writeText(mapString) -} +} \ No newline at end of file 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 2251c2e6..8c018da4 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 @@ -13,10 +13,6 @@ 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 kotlin.reflect.KClass - // Get the hl7 resources val contextR4 = FhirContext.forR4() @@ -280,21 +276,21 @@ class Group( if (answerExpression != null) { if ( answerExpression.isNotEmpty() && - answerExpression.isNotBlank() && - answerExpression != "''" + 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") + answerType != propertyType && + propertyType + ?.canHandleConversion( + answerType ?: "", + ) + ?.not() == true && + answerExpression.startsWith("evaluate") ) { println( "Failed type matching --> ${instruction!!.fullPropertyPath()} of type $answerType != $propertyType" @@ -353,50 +349,6 @@ fun generateStructureMapLine( resource: Resource, extractionResources: HashMap, ) { - val fieldPath = row.getCell(4)?.stringCellValue ?: "" - val cellValue = row.getCell(0)?.stringCellValue ?: "" - - // Determine the target FHIR data type - val targetDataType = determineFhirDataType(cellValue) - - // Generate the mapping line for the StructureMap - structureMapBody.append("src -> entity.$fieldPath = ") - - // Handle different data types - when (targetDataType) { - "string" -> { - structureMapBody.append("create('string').value = '${cellValue.escapeQuotes()}'") - } - - "integer" -> { - structureMapBody.append("create('integer').value = ${cellValue.toIntOrNull() ?: 0}") - } - - "boolean" -> { - val booleanValue = if (cellValue.equals("true", ignoreCase = true)) "true" else "false" - structureMapBody.append("create('boolean').value = $booleanValue") - } - - "date" -> { - // Handle date type - structureMapBody.append("create('date').value = '${cellValue.escapeQuotes()}'") - } - - "decimal" -> { - // Handle decimal type - structureMapBody.append("create('decimal').value = ${cellValue.toDoubleOrNull() ?: 0.0}") - } - - "code" -> { - // Handle code type - structureMapBody.append("create('code').value = '${cellValue.escapeQuotes()}'") - } - - else -> { - structureMapBody.append("create('unsupportedDataType').value = '${cellValue.escapeQuotes()}'") - } - } - structureMapBody.appendNewLine() row.forEachIndexed { index, cell -> val cellValue = cell.stringCellValue val fieldPath = row.getCell(4).stringCellValue @@ -437,87 +389,6 @@ fun determineFhirDataType(cellValue: String): String { } } -fun String.escapeQuotes(): String { - return this.replace("'", "\\'") -} - -fun determineFhirDataType(input: String?): String { - if (input.isNullOrEmpty()) { - return "Invalid Input: Null or Empty String" - } - - // Clean and prepare the input for matching - val cleanedValue = input.trim().toLowerCase() - - // Regular Expressions for FHIR Data Types - val booleanRegex = "^(true|false)\$".toRegex(RegexOption.IGNORE_CASE) - val integerRegex = "^-?\\d+\$".toRegex() - val decimalRegex = "^-?\\d*\\.\\d+\$".toRegex() - val dateRegex = "^\\d{4}-\\d{2}-\\d{2}\$".toRegex() // YYYY-MM-DD - val dateTimeRegex = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?\$".toRegex() // YYYY-MM-DDThh:mm:ssZ - val stringRegex = "^[\\w\\s]+\$".toRegex() - val quantityRegex = "^\\d+\\s?[a-zA-Z]+\$".toRegex() // e.g., "70 kg" - val codeableConceptRegex = "^[\\w\\s]+\$".toRegex() // Simplified for now - val codingRegex = "^\\w+\\|\$".toRegex() // Simplified for now - val referenceRegex = "^\\w+/\\w+\$".toRegex() // e.g., "Patient/123" - val periodRegex = "^\\d{4}-\\d{2}-\\d{2}\\/\\d{4}-\\d{2}-\\d{2}\$".toRegex() // e.g., "2023-01-01/2023-12-31" - val timingRegex = "^\\d+[a-zA-Z]+\$".toRegex() // Simplified for now - val rangeRegex = "^\\d+-\\d+\$".toRegex() // e.g., "10-20" - val annotationRegex = """^.*\s+\S+""".toRegex() // A basic regex for general text or comments - val attachmentRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 encoded string (could be more complex) - val base64BinaryRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Similar to attachment, but could have specific markers - val contactPointRegex = """^\+?[1-9]\d{1,14}$""".toRegex() // Regex for phone numbers (E.164 format) - val humanNameRegex = """^[A-Za-z\s'-]+$""".toRegex() // Simple regex for names - val addressRegex = """^\d+\s[A-Za-z]+\s[A-Za-z]+""".toRegex() // Basic address pattern - val durationRegex = """^\d+\s(hour|minute|second|day)$""".toRegex() // Duration like "1 hour" - val moneyRegex = """^\d+(\.\d{2})?\s[A-Z]{3}$""".toRegex() // Money format like "100.00 USD" - val ratioRegex = """^\d+:\d+$""".toRegex() // Simple ratio like "1:1000" - val signatureRegex = """^[A-Za-z0-9+/=]+$""".toRegex() // Base64 signature - val identifierRegex = """^[A-Za-z0-9-]+$""".toRegex() // Identifier format - val uriRegex = """^https?://[^\s/$.?#].[^\s]*$""".toRegex() // Simple URI format - val uuidRegex = """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$""".toRegex() // UUID format - val instantRegex = """^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$""".toRegex() // ISO 8601 instant format - val narrativeRegex = """.*<\/div>""".toRegex() // Narrative XHTML content - val sampledDataRegex = """^.*\s+\S+""".toRegex() // Placeholder regex for complex observation data - val backboneElementRegex = """^.*$""".toRegex() // Catch-all for complex structures (requires specific context) - // Detect and Return FHIR Data Type - return when { - booleanRegex.matches(cleanedValue) -> "Boolean" - integerRegex.matches(cleanedValue) -> "Integer" - decimalRegex.matches(cleanedValue) -> "Decimal" - dateRegex.matches(cleanedValue) -> "Date" - dateTimeRegex.matches(cleanedValue) -> "DateTime" - quantityRegex.matches(input) -> "Quantity" - codeableConceptRegex.matches(input) -> "CodeableConcept" - codingRegex.matches(input) -> "Coding" - identifierRegex.matches(input) -> "Identifier" - referenceRegex.matches(input) -> "Reference" - periodRegex.matches(input) -> "Period" - timingRegex.matches(input) -> "Timing" - rangeRegex.matches(input) -> "Range" - stringRegex.matches(input) -> "String" - annotationRegex.matches(input) -> "Annotation" - attachmentRegex.matches(input) -> "Attachment" - base64BinaryRegex.matches(input) -> "Base64Binary" - contactPointRegex.matches(input) -> "ContactPoint" - humanNameRegex.matches(input) -> "HumanName" - addressRegex.matches(input) -> "Address" - durationRegex.matches(input) -> "Duration" - moneyRegex.matches(input) -> "Money" - ratioRegex.matches(input) -> "Ratio" - signatureRegex.matches(input) -> "Signature" - identifierRegex.matches(input) -> "Identifier" - uriRegex.matches(input) -> "Uri" - uuidRegex.matches(input) -> "Uuid" - instantRegex.matches(input) -> "Instant" - narrativeRegex.matches(input) -> "Narrative" - sampledDataRegex.matches(input) -> "SampledData" - backboneElementRegex.matches(input) -> "BackboneElement" - else -> "String" - } -} - - fun StringBuilder.appendNewLine(): StringBuilder { append(System.lineSeparator()) return this @@ -564,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 } } @@ -630,46 +501,12 @@ fun inferType(parentClass: Class<*>?, parts: List, index: Int): String? fun String.isMultipleTypes(): Boolean = this == "Type" -// Assuming a mock annotation to simulate the @Child.type annotation in FHIR -annotation class Child(val type: KClass) +// TODO: Finish this. Use the annotation @Chid.type fun String.getPossibleTypes(): List { - val clazz = Class.forName("org.hl7.fhir.r4.model.$this") - val possibleTypes = mutableListOf() - - clazz.declaredFields.forEach { field -> - val annotation = field.annotations.find { it is Child } as? Child - annotation?.let { - val typeInstance = it.type.java.getDeclaredConstructor().newInstance() - possibleTypes.add(typeInstance) - } - } - - return possibleTypes - + 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", - "DateTimeType" to "StringType", - "TimeType" to "StringType", - "InstantType" to "DateTimeType", - "UriType" to "StringType", - "UuidType" to "StringType", - "CodeType" to "StringType", - "MarkdownType" to "StringType", - "Base64BinaryType" to "StringType", - "OidType" to "StringType", - "PositiveIntType" to "IntegerType", - "UnsignedIntType" to "IntegerType", - "IdType" to "StringType", - "CanonicalType" to "StringType" val propertyClass = Class.forName("org.hl7.fhir.r4.model.$this") val targetType2 = if (sourceType == "StringType") String::class.java @@ -689,19 +526,6 @@ fun String.canHandleConversion(sourceType: String): Boolean { } } - // Check if the source type can be converted to any of the possible types for this target type - val possibleTypes = this.getPossibleTypes() - possibleTypes.forEach { possibleType -> - if (possibleType::class.simpleName == sourceType) { - return true - } - } - - try { - propertyClass.getDeclaredMethod("fromCode", targetType2) - } catch (ex: NoSuchMethodException) { - return false - } try { propertyClass.getDeclaredMethod("fromCode", targetType2) } catch (ex: NoSuchMethodException) { @@ -711,7 +535,6 @@ fun String.canHandleConversion(sourceType: String): Boolean { return true } - fun String.getParentResource(): String? { return substring(0, lastIndexOf('.')) } @@ -720,4 +543,4 @@ fun String.getResourceProperty(): String? { return substring(lastIndexOf('.') + 1) } -fun String.getFhirType(): String = replace("Type", "").lowercase() +fun String.getFhirType(): String = replace("Type", "").lowercase() \ No newline at end of file diff --git a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt index 6d4f460f..5f066cc1 100644 --- a/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt +++ b/sm-gen/src/test/kotlin/org/smartregister/fhir/structuremaptool/TransformSupportServicesTest.kt @@ -1,6 +1,6 @@ package org.smartregister.fhir.structuremaptool -import io.mockk.mockk +import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.CarePlan import org.hl7.fhir.r4.model.Encounter @@ -15,11 +15,13 @@ import org.junit.jupiter.api.BeforeEach import kotlin.test.Test class TransformSupportServicesTest{ - lateinit var transformSupportServices: TransformSupportServices + private lateinit var transformSupportServices: TransformSupportServices @BeforeEach fun setUp() { - transformSupportServices = TransformSupportServices(mockk()) + val simpleWorkerContext = SimpleWorkerContext() + + transformSupportServices = TransformSupportServices(simpleWorkerContext) } diff --git a/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt b/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt index 4c91ac85..58c8738b 100644 --- a/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt +++ b/sm-gen/src/test/kotlin/utils/DetermineFhirDataTypeTest.kt @@ -9,7 +9,7 @@ class DetermineFhirDataTypeTest { @Test fun testDetermineFhirDataType() { // Test Null or Empty Input - assertEquals("Invalid Input: Null or Empty String", determineFhirDataType(null)) + assertEquals("Invalid Input: Null or Empty String", determineFhirDataType(null.toString())) assertEquals("Invalid Input: Null or Empty String", determineFhirDataType("")) // Test Boolean