• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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