Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle unsupported datatypes #280

Merged
merged 14 commits into from
Oct 17, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -152,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*/
sharon2719 marked this conversation as resolved.
Show resolved Hide resolved

sb.append(structureMapHeader)
sb.appendNewLine().appendNewLine().appendNewLine()
Expand All @@ -170,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<String>()
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()
Expand Down Expand Up @@ -221,6 +221,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)) {
sharon2719 marked this conversation as resolved.
Show resolved Hide resolved
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<String>): Map<String, MutableList<Instruction>> {
val fieldMappingsSheet = workbook.getSheet("Field Mappings")
val resourceConversionInstructions = hashMapOf<String, MutableList<Instruction>>()

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 {
Expand Down
165 changes: 127 additions & 38 deletions sm-gen/src/main/kotlin/org.smartregister.fhir.structuremaptool/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -320,50 +321,105 @@ fun generateStructureMapLine(
resource: Resource,
extractionResources: HashMap<String, Resource>
) {
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}")
}
"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()}'")
}
structureMapBody.appendNewLine()
}

structureMapBody.append(";")
}

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"
}

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\s+xmlns="http://www.w3.org/1999/xhtml">.*<\/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 StringBuilder.appendNewLine(): StringBuilder {
append(System.lineSeparator())
return this
Expand Down Expand Up @@ -475,22 +531,46 @@ fun inferType(parentClass: Class<*>?, parts: List<String>, 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<out Type>)
fun String.getPossibleTypes(): List<Type> {
return listOf()
val clazz = Class.forName("org.hl7.fhir.r4.model.$this")
val possibleTypes = mutableListOf<Type>()

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 {
Expand All @@ -499,6 +579,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) {
Expand All @@ -508,6 +596,7 @@ fun String.canHandleConversion(sourceType: String): Boolean {
return true
}


fun String.getParentResource(): String? {
return substring(0, lastIndexOf('.'))
}
Expand Down
Loading