1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * ```
8 * http://www.apache.org/licenses/LICENSE-2.0
9 * ```
10 *
11 * Unless required by applicable law or agreed to in writing, software distributed under the License
12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 package com.android.healthconnect.controller.dataentries.formatters.medical
17
18 import android.content.Context
19 import com.android.healthconnect.controller.R
20 import dagger.hilt.android.qualifiers.ApplicationContext
21 import javax.inject.Inject
22 import javax.inject.Singleton
23 import org.json.JSONObject
24
25 /** Extracts the most relevant display name from the FHIR resource. */
26 @Singleton
27 class DisplayNameExtractor @Inject constructor(@ApplicationContext private val context: Context) {
28
29 private lateinit var unknownResource: String
30 private lateinit var fhirData: JSONObject
31
32 companion object {
33 private const val RESOURCE_TYPE = "resourceType"
34 private const val NAME = "name"
35 private const val USUAL = "usual"
36 private const val OFFICIAL = "official"
37 private const val USE = "use"
38 private const val TEXT = "text"
39 private const val FAMILY = "family"
40 private const val GIVEN = "given"
41 private const val CLASS = "class"
42 private const val SERVICE_TYPE = "serviceType"
43 private const val CODE = "code"
44 private const val CODING = "coding"
45 private const val DISPLAY = "display"
46 private const val SYSTEM = "system"
47 private const val SNOMED_SYSTEM = "http://snomed.info/sct"
48 private const val LOINC_SYSTEM = "http://loinc.org"
49 private const val CVX_SYSTEM = "http://hl7.org/fhir/sid/cvx"
50 private const val VACCINE_CODE = "vaccineCode"
51 private const val MEDICATION_CODEABLE_CONCEPT = "medicationCodeableConcept"
52 private const val MEDICATION_REFERENCE = "medicationReference"
53 private const val MEDICATION = "medication"
54 private const val ALIAS = "alias"
55 private const val PREFIX = "prefix"
56 private const val SPECIALTY = "specialty"
57
58 private const val PATIENT = "Patient"
59 private const val ENCOUNTER = "Encounter"
60 private const val CONDITION = "Condition"
61 private const val PROCEDURE = "Procedure"
62 private const val OBSERVATION = "Observation"
63 private const val ALLERGY_INTOLERANCE = "AllergyIntolerance"
64 private const val IMMUNIZATION = "Immunization"
65 private const val MEDICATION_REQUEST = "MedicationRequest"
66 private const val MEDICATION_STATEMENT = "MedicationStatement"
67 private const val MEDICATION_RESOURCE = "Medication"
68 private const val LOCATION = "Location"
69 private const val ORGANIZATION = "Organization"
70 private const val PRACTITIONER_ROLE = "PractitionerRole"
71 private const val PRACTITIONER = "Practitioner"
72 }
73
74 fun getDisplayName(fhirResourceJson: String): String {
75 unknownResource = context.getString(R.string.unkwown_resource)
76 fhirData = JSONObject(fhirResourceJson)
77 val resourceType = fhirData.optString(RESOURCE_TYPE)
78
79 return when (resourceType) {
80 ALLERGY_INTOLERANCE,
81 CONDITION,
82 MEDICATION_RESOURCE,
83 PROCEDURE -> extractCodeDisplay(CODE, listOf(SNOMED_SYSTEM))
84 ENCOUNTER -> extractEncounter()
85 IMMUNIZATION -> extractCodeDisplay(VACCINE_CODE, listOf(CVX_SYSTEM, SNOMED_SYSTEM))
86 LOCATION,
87 ORGANIZATION -> extractNameOrAlias()
88 MEDICATION_REQUEST,
89 MEDICATION_STATEMENT -> extractMedicationConcept()
90 OBSERVATION -> extractCodeDisplay(CODE, listOf(LOINC_SYSTEM, SNOMED_SYSTEM))
91 PATIENT -> extractPatient()
92 PRACTITIONER -> extractPractitioner()
93 PRACTITIONER_ROLE -> extractPractitionerRole()
94 else -> unknownResource
95 }
96 }
97
98 private fun extractCodeDisplay(code: String, codings: List<String>): String {
99 val codeOrVaccineCode = fhirData.optJSONObject(code) ?: return unknownResource
100 return codeOrVaccineCode.optString(TEXT).takeIf { !it.isNullOrEmpty() }
101 ?: codeOrVaccineCode.optJSONArray(CODING)?.let { codingArray ->
102 for (codingSystem in codings) {
103 for (i in 0 until codingArray.length()) {
104 val coding = codingArray.getJSONObject(i)
105 if (coding.optString(SYSTEM) == codingSystem) {
106 return coding.optString(DISPLAY)
107 }
108 }
109 }
110 codingArray.optJSONObject(0)?.optString(DISPLAY)
111 }
112 ?: unknownResource
113 }
114
115 private fun extractEncounter(): String {
116 val classText = fhirData.optJSONObject(CLASS)?.optStringOrNull(DISPLAY)
117 val serviceText = extractTextOrDisplay(SERVICE_TYPE)
118 return if (classText == null && serviceText == null) {
119 unknownResource
120 } else {
121 concat(classText, serviceText, delim = " - ")
122 }
123 }
124
125 private fun extractTextOrDisplay(key: String) =
126 (fhirData.optJSONObject(key)?.optStringOrNull(TEXT)
127 ?: fhirData
128 .optJSONObject(key)
129 ?.optJSONArray(CODING)
130 ?.optJSONObject(0)
131 ?.optString(DISPLAY))
132
133 private fun extractNameOrAlias() =
134 (fhirData.optStringOrNull(NAME)
135 ?: fhirData.optJSONArray(ALIAS)?.optString(0).takeIf { !it.isNullOrEmpty() }
136 ?: unknownResource)
137
138 private fun extractMedicationConcept(): String {
139 val medicationReference = fhirData.optJSONObject(MEDICATION_REFERENCE)
140 val medicationCodeableConcept = fhirData.optJSONObject(MEDICATION_CODEABLE_CONCEPT)
141 return medicationReference?.optStringOrNull(DISPLAY)
142 ?: medicationCodeableConcept?.optStringOrNull(TEXT)
143 ?: medicationCodeableConcept?.optJSONArray(CODING)?.let { codingArray ->
144 for (i in 0 until codingArray.length()) {
145 val coding = codingArray.getJSONObject(i)
146 if (coding.optString(SYSTEM) == SNOMED_SYSTEM) {
147 return coding.optString(DISPLAY)
148 }
149 }
150 codingArray.optJSONObject(0)?.optString(DISPLAY)
151 }
152 ?: unknownResource
153 }
154
155 private fun extractPatient(): String {
156 val names = fhirData.optJSONArray(NAME) ?: return unknownResource
157 for (i in 0 until names.length()) {
158 val name = names.getJSONObject(i)
159 if (name.optString(USE) == USUAL) {
160 return name.optString(TEXT)
161 ?: concat(name.optJSONArray(GIVEN)?.optString(0), name.optString(FAMILY))
162 } else if (name.optString(USE) == OFFICIAL) {
163 return name.optString(TEXT)
164 ?: concat(name.optJSONArray(GIVEN)?.optString(0), name.optString(FAMILY))
165 }
166 }
167 if (names.length() > 0) {
168 val firstName = names.getJSONObject(0)
169 return firstName.optString(TEXT).takeIf { !it.isNullOrEmpty() }
170 ?: concat(firstName.optJSONArray(GIVEN)?.optString(0), firstName.optString(FAMILY))
171 }
172 return unknownResource
173 }
174
175 private fun extractPractitioner(): String {
176 val names = fhirData.optJSONArray(NAME) ?: return unknownResource
177 for (i in 0 until names.length()) {
178 val name = names.getJSONObject(i)
179 if (name.optString(USE) == USUAL) {
180 return concatWholeName(name)
181 } else if (name.optString(USE) == OFFICIAL) {
182 return concatWholeName(name)
183 }
184 }
185 if (names.length() > 0) {
186 val name = names.getJSONObject(0)
187 return concatWholeName(name)
188 }
189 return unknownResource
190 }
191
192 private fun concatWholeName(name: JSONObject): String =
193 name.optStringOrNull(TEXT)
194 ?: concat(
195 name.optJSONArray(PREFIX)?.optString(0),
196 name.optJSONArray(GIVEN)?.optString(0),
197 name.optString(FAMILY),
198 )
199
200 private fun extractPractitionerRole(): String {
201 val codeText =
202 fhirData.optJSONArray(CODE)?.optJSONObject(0)?.optStringOrNull(TEXT)
203 ?: fhirData
204 .optJSONArray(CODE)
205 ?.optJSONObject(0)
206 ?.optJSONArray(CODING)
207 ?.optJSONObject(0)
208 ?.optString(DISPLAY)
209 val specialtyText =
210 fhirData.optJSONArray(SPECIALTY)?.optJSONObject(0)?.optStringOrNull(TEXT)
211 ?: fhirData
212 .optJSONArray(SPECIALTY)
213 ?.optJSONObject(0)
214 ?.optJSONArray(CODING)
215 ?.optJSONObject(0)
216 ?.optString(DISPLAY)
217 return if (codeText == null && specialtyText == null) {
218 unknownResource
219 } else {
220 concat(codeText, specialtyText, delim = " - ")
221 }
222 }
223
224 private fun concat(vararg args: String?, delim: String = " "): String {
225 return args.filterNotNull().filter { it.isNotBlank() }.joinToString(delim)
226 }
227 }
228
JSONObjectnull229 private fun JSONObject.optStringOrNull(s: String): String? {
230 return this.optString(s).takeIf { it != "" }
231 }
232