001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 025import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 026import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 027import ca.uhn.fhir.jpa.model.util.CodeSystemHash; 028import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc; 029import ca.uhn.fhir.jpa.search.lastn.json.CodeJson; 030import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson; 031import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 032import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef; 033import ca.uhn.fhir.parser.IParser; 034import org.hl7.fhir.instance.model.api.IBase; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036import org.springframework.beans.factory.annotation.Autowired; 037 038import java.util.ArrayList; 039import java.util.Date; 040import java.util.List; 041import java.util.Objects; 042import java.util.Optional; 043import java.util.UUID; 044 045public class ObservationLastNIndexPersistSvc { 046 047 @Autowired 048 private ISearchParamExtractor mySearchParameterExtractor; 049 050 @Autowired(required = false) 051 private IElasticsearchSvc myElasticsearchSvc; 052 053 @Autowired 054 private JpaStorageSettings myConfig; 055 056 @Autowired 057 private FhirContext myContext; 058 059 public void indexObservation(IBaseResource theResource) { 060 061 if (myElasticsearchSvc == null) { 062 // Elasticsearch is not enabled and therefore no index needs to be updated. 063 return; 064 } 065 066 List<IBase> subjectReferenceElement = 067 mySearchParameterExtractor.extractValues("Observation.subject", theResource); 068 String subjectId = subjectReferenceElement.stream() 069 .map(refElement -> 070 mySearchParameterExtractor.extractReferenceLinkFromResource(refElement, "Observation.subject")) 071 .filter(Objects::nonNull) 072 .map(PathAndRef::getRef) 073 .filter(Objects::nonNull) 074 .map(subjectRef -> subjectRef.getReferenceElement().getValue()) 075 .filter(Objects::nonNull) 076 .findFirst() 077 .orElse(null); 078 079 Date effectiveDtm = null; 080 List<IBase> effectiveDateElement = 081 mySearchParameterExtractor.extractValues("Observation.effective", theResource); 082 if (effectiveDateElement.size() > 0) { 083 effectiveDtm = mySearchParameterExtractor.extractDateFromResource( 084 effectiveDateElement.get(0), "Observation.effective"); 085 } 086 087 List<IBase> observationCodeCodeableConcepts = 088 mySearchParameterExtractor.extractValues("Observation.code", theResource); 089 090 // Only index for lastn if Observation has a code 091 if (observationCodeCodeableConcepts.size() == 0) { 092 return; 093 } 094 095 List<IBase> observationCategoryCodeableConcepts = 096 mySearchParameterExtractor.extractValues("Observation.category", theResource); 097 098 createOrUpdateIndexedObservation( 099 theResource, 100 effectiveDtm, 101 subjectId, 102 observationCodeCodeableConcepts, 103 observationCategoryCodeableConcepts); 104 } 105 106 private void createOrUpdateIndexedObservation( 107 IBaseResource theResource, 108 Date theEffectiveDtm, 109 String theSubjectId, 110 List<IBase> theObservationCodeCodeableConcepts, 111 List<IBase> theObservationCategoryCodeableConcepts) { 112 String resourcePID = theResource.getIdElement().getIdPart(); 113 114 // Determine if an index already exists for Observation: 115 ObservationJson indexedObservation = null; 116 if (resourcePID != null) { 117 indexedObservation = myElasticsearchSvc.getObservationDocument(resourcePID); 118 } 119 if (indexedObservation == null) { 120 indexedObservation = new ObservationJson(); 121 } 122 123 indexedObservation.setEffectiveDtm(theEffectiveDtm); 124 indexedObservation.setIdentifier(resourcePID); 125 if (myConfig.isStoreResourceInHSearchIndex()) { 126 indexedObservation.setResource(encodeResource(theResource)); 127 } 128 indexedObservation.setSubject(theSubjectId); 129 130 addCodeToObservationIndex(theObservationCodeCodeableConcepts, indexedObservation); 131 132 addCategoriesToObservationIndex(theObservationCategoryCodeableConcepts, indexedObservation); 133 134 myElasticsearchSvc.createOrUpdateObservationIndex(resourcePID, indexedObservation); 135 } 136 137 private String encodeResource(IBaseResource theResource) { 138 IParser parser = myContext.newJsonParser(); 139 return parser.encodeResourceToString(theResource); 140 } 141 142 private void addCodeToObservationIndex( 143 List<IBase> theObservationCodeCodeableConcepts, ObservationJson theIndexedObservation) { 144 // Determine if a Normalized ID was created previously for Observation Code 145 String existingObservationCodeNormalizedId = 146 getCodeCodeableConceptId(theObservationCodeCodeableConcepts.get(0)); 147 148 // Create/update normalized Observation Code index record 149 CodeJson codeableConceptField = 150 getCodeCodeableConcept(theObservationCodeCodeableConcepts.get(0), existingObservationCodeNormalizedId); 151 152 myElasticsearchSvc.createOrUpdateObservationCodeIndex( 153 codeableConceptField.getCodeableConceptId(), codeableConceptField); 154 155 theIndexedObservation.setCode(codeableConceptField); 156 } 157 158 private void addCategoriesToObservationIndex( 159 List<IBase> observationCategoryCodeableConcepts, ObservationJson indexedObservation) { 160 // Build CodeableConcept entities for Observation.Category 161 List<CodeJson> categoryCodeableConceptEntities = new ArrayList<>(); 162 for (IBase categoryCodeableConcept : observationCategoryCodeableConcepts) { 163 // Build CodeableConcept entities for each category CodeableConcept 164 categoryCodeableConceptEntities.add(getCategoryCodeableConceptEntities(categoryCodeableConcept)); 165 } 166 indexedObservation.setCategories(categoryCodeableConceptEntities); 167 } 168 169 private CodeJson getCategoryCodeableConceptEntities(IBase theValue) { 170 String text = mySearchParameterExtractor.getDisplayTextFromCodeableConcept(theValue); 171 CodeJson categoryCodeableConcept = new CodeJson(); 172 categoryCodeableConcept.setCodeableConceptText(text); 173 174 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 175 for (IBase nextCoding : codings) { 176 addCategoryCoding(nextCoding, categoryCodeableConcept); 177 } 178 return categoryCodeableConcept; 179 } 180 181 private CodeJson getCodeCodeableConcept(IBase theValue, String observationCodeNormalizedId) { 182 String text = mySearchParameterExtractor.getDisplayTextFromCodeableConcept(theValue); 183 CodeJson codeCodeableConcept = new CodeJson(); 184 codeCodeableConcept.setCodeableConceptText(text); 185 codeCodeableConcept.setCodeableConceptId(observationCodeNormalizedId); 186 187 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 188 for (IBase nextCoding : codings) { 189 addCodeCoding(nextCoding, codeCodeableConcept); 190 } 191 192 return codeCodeableConcept; 193 } 194 195 private String getCodeCodeableConceptId(IBase theValue) { 196 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 197 Optional<String> codeCodeableConceptIdOptional = Optional.empty(); 198 199 for (IBase nextCoding : codings) { 200 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding( 201 "Observation", 202 new RuntimeSearchParam(null, null, "code", null, null, null, null, null, null, null), 203 nextCoding); 204 if (param != null) { 205 String system = param.getSystem(); 206 String code = param.getValue(); 207 String text = mySearchParameterExtractor.getDisplayTextForCoding(nextCoding); 208 209 String codeSystemHash = String.valueOf(CodeSystemHash.hashCodeSystem(system, code)); 210 CodeJson codeCodeableConceptDocument = 211 myElasticsearchSvc.getObservationCodeDocument(codeSystemHash, text); 212 if (codeCodeableConceptDocument != null) { 213 codeCodeableConceptIdOptional = Optional.of(codeCodeableConceptDocument.getCodeableConceptId()); 214 break; 215 } 216 } 217 } 218 219 return codeCodeableConceptIdOptional.orElse(UUID.randomUUID().toString()); 220 } 221 222 private void addCategoryCoding(IBase theValue, CodeJson theCategoryCodeableConcept) { 223 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding( 224 "Observation", 225 new RuntimeSearchParam(null, null, "category", null, null, null, null, null, null, null), 226 theValue); 227 if (param != null) { 228 String system = param.getSystem(); 229 String code = param.getValue(); 230 String text = mySearchParameterExtractor.getDisplayTextForCoding(theValue); 231 theCategoryCodeableConcept.addCoding(system, code, text); 232 } 233 } 234 235 private void addCodeCoding(IBase theValue, CodeJson theObservationCode) { 236 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding( 237 "Observation", 238 new RuntimeSearchParam(null, null, "code", null, null, null, null, null, null, null), 239 theValue); 240 if (param != null) { 241 String system = param.getSystem(); 242 String code = param.getValue(); 243 String text = mySearchParameterExtractor.getDisplayTextForCoding(theValue); 244 theObservationCode.addCoding(system, code, text); 245 } 246 } 247 248 public void deleteObservationIndex(IBasePersistedResource theEntity) { 249 if (myElasticsearchSvc == null) { 250 // Elasticsearch is not enabled and therefore no index needs to be updated. 251 return; 252 } 253 254 ObservationJson deletedObservationLastNEntity = 255 myElasticsearchSvc.getObservationDocument(theEntity.getIdDt().getIdPart()); 256 if (deletedObservationLastNEntity != null) { 257 myElasticsearchSvc.deleteObservationDocument(deletedObservationLastNEntity.getIdentifier()); 258 } 259 } 260}