
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 = mySearchParameterExtractor.extractValues("Observation.subject", theResource); 067 String subjectId = subjectReferenceElement.stream() 068 .map(refElement -> mySearchParameterExtractor.extractReferenceLinkFromResource(refElement, "Observation.subject")) 069 .filter(Objects::nonNull) 070 .map(PathAndRef::getRef) 071 .filter(Objects::nonNull) 072 .map(subjectRef -> subjectRef.getReferenceElement().getValue()) 073 .filter(Objects::nonNull) 074 .findFirst().orElse(null); 075 076 Date effectiveDtm = null; 077 List<IBase> effectiveDateElement = mySearchParameterExtractor.extractValues("Observation.effective", theResource); 078 if (effectiveDateElement.size() > 0) { 079 effectiveDtm = mySearchParameterExtractor.extractDateFromResource(effectiveDateElement.get(0), "Observation.effective"); 080 } 081 082 List<IBase> observationCodeCodeableConcepts = mySearchParameterExtractor.extractValues("Observation.code", theResource); 083 084 // Only index for lastn if Observation has a code 085 if (observationCodeCodeableConcepts.size() == 0) { 086 return; 087 } 088 089 List<IBase> observationCategoryCodeableConcepts = mySearchParameterExtractor.extractValues("Observation.category", theResource); 090 091 createOrUpdateIndexedObservation(theResource, effectiveDtm, subjectId, observationCodeCodeableConcepts, observationCategoryCodeableConcepts); 092 093 } 094 095 private void createOrUpdateIndexedObservation(IBaseResource theResource, Date theEffectiveDtm, String theSubjectId, 096 List<IBase> theObservationCodeCodeableConcepts, 097 List<IBase> theObservationCategoryCodeableConcepts) { 098 String resourcePID = theResource.getIdElement().getIdPart(); 099 100 // Determine if an index already exists for Observation: 101 ObservationJson indexedObservation = null; 102 if (resourcePID != null) { 103 indexedObservation = myElasticsearchSvc.getObservationDocument(resourcePID); 104 } 105 if (indexedObservation == null) { 106 indexedObservation = new ObservationJson(); 107 } 108 109 indexedObservation.setEffectiveDtm(theEffectiveDtm); 110 indexedObservation.setIdentifier(resourcePID); 111 if (myConfig.isStoreResourceInHSearchIndex()) { 112 indexedObservation.setResource(encodeResource(theResource)); 113 } 114 indexedObservation.setSubject(theSubjectId); 115 116 addCodeToObservationIndex(theObservationCodeCodeableConcepts, indexedObservation); 117 118 addCategoriesToObservationIndex(theObservationCategoryCodeableConcepts, indexedObservation); 119 120 myElasticsearchSvc.createOrUpdateObservationIndex(resourcePID, indexedObservation); 121 122 } 123 124 private String encodeResource(IBaseResource theResource) { 125 IParser parser = myContext.newJsonParser(); 126 return parser.encodeResourceToString(theResource); 127 } 128 129 private void addCodeToObservationIndex(List<IBase> theObservationCodeCodeableConcepts, 130 ObservationJson theIndexedObservation) { 131 // Determine if a Normalized ID was created previously for Observation Code 132 String existingObservationCodeNormalizedId = getCodeCodeableConceptId(theObservationCodeCodeableConcepts.get(0)); 133 134 // Create/update normalized Observation Code index record 135 CodeJson codeableConceptField = 136 getCodeCodeableConcept(theObservationCodeCodeableConcepts.get(0), 137 existingObservationCodeNormalizedId); 138 139 myElasticsearchSvc.createOrUpdateObservationCodeIndex(codeableConceptField.getCodeableConceptId(), codeableConceptField); 140 141 theIndexedObservation.setCode(codeableConceptField); 142 } 143 144 private void addCategoriesToObservationIndex(List<IBase> observationCategoryCodeableConcepts, 145 ObservationJson indexedObservation) { 146 // Build CodeableConcept entities for Observation.Category 147 List<CodeJson> categoryCodeableConceptEntities = new ArrayList<>(); 148 for (IBase categoryCodeableConcept : observationCategoryCodeableConcepts) { 149 // Build CodeableConcept entities for each category CodeableConcept 150 categoryCodeableConceptEntities.add(getCategoryCodeableConceptEntities(categoryCodeableConcept)); 151 } 152 indexedObservation.setCategories(categoryCodeableConceptEntities); 153 } 154 155 private CodeJson getCategoryCodeableConceptEntities(IBase theValue) { 156 String text = mySearchParameterExtractor.getDisplayTextFromCodeableConcept(theValue); 157 CodeJson categoryCodeableConcept = new CodeJson(); 158 categoryCodeableConcept.setCodeableConceptText(text); 159 160 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 161 for (IBase nextCoding : codings) { 162 addCategoryCoding(nextCoding, categoryCodeableConcept); 163 } 164 return categoryCodeableConcept; 165 } 166 167 private CodeJson getCodeCodeableConcept(IBase theValue, String observationCodeNormalizedId) { 168 String text = mySearchParameterExtractor.getDisplayTextFromCodeableConcept(theValue); 169 CodeJson codeCodeableConcept = new CodeJson(); 170 codeCodeableConcept.setCodeableConceptText(text); 171 codeCodeableConcept.setCodeableConceptId(observationCodeNormalizedId); 172 173 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 174 for (IBase nextCoding : codings) { 175 addCodeCoding(nextCoding, codeCodeableConcept); 176 } 177 178 return codeCodeableConcept; 179 } 180 181 private String getCodeCodeableConceptId(IBase theValue) { 182 List<IBase> codings = mySearchParameterExtractor.getCodingsFromCodeableConcept(theValue); 183 Optional<String> codeCodeableConceptIdOptional = Optional.empty(); 184 185 for (IBase nextCoding : codings) { 186 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding("Observation", 187 new RuntimeSearchParam(null, null, "code", null, null, null, 188 null, null, null, null), 189 nextCoding); 190 if (param != null) { 191 String system = param.getSystem(); 192 String code = param.getValue(); 193 String text = mySearchParameterExtractor.getDisplayTextForCoding(nextCoding); 194 195 String codeSystemHash = String.valueOf(CodeSystemHash.hashCodeSystem(system, code)); 196 CodeJson codeCodeableConceptDocument = myElasticsearchSvc.getObservationCodeDocument(codeSystemHash, text); 197 if (codeCodeableConceptDocument != null) { 198 codeCodeableConceptIdOptional = Optional.of(codeCodeableConceptDocument.getCodeableConceptId()); 199 break; 200 } 201 } 202 } 203 204 return codeCodeableConceptIdOptional.orElse(UUID.randomUUID().toString()); 205 } 206 207 private void addCategoryCoding(IBase theValue, CodeJson theCategoryCodeableConcept) { 208 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding("Observation", 209 new RuntimeSearchParam(null, null, "category", null, null, null, null, null, null, null), 210 theValue); 211 if (param != null) { 212 String system = param.getSystem(); 213 String code = param.getValue(); 214 String text = mySearchParameterExtractor.getDisplayTextForCoding(theValue); 215 theCategoryCodeableConcept.addCoding(system, code, text); 216 } 217 } 218 219 private void addCodeCoding(IBase theValue, CodeJson theObservationCode) { 220 ResourceIndexedSearchParamToken param = mySearchParameterExtractor.createSearchParamForCoding("Observation", 221 new RuntimeSearchParam(null, null, "code", null, null, null, null, null, null, null), 222 theValue); 223 if (param != null) { 224 String system = param.getSystem(); 225 String code = param.getValue(); 226 String text = mySearchParameterExtractor.getDisplayTextForCoding(theValue); 227 theObservationCode.addCoding(system, code, text); 228 } 229 } 230 231 public void deleteObservationIndex(IBasePersistedResource theEntity) { 232 if (myElasticsearchSvc == null) { 233 // Elasticsearch is not enabled and therefore no index needs to be updated. 234 return; 235 } 236 237 ObservationJson deletedObservationLastNEntity = myElasticsearchSvc.getObservationDocument(theEntity.getIdDt().getIdPart()); 238 if (deletedObservationLastNEntity != null) { 239 myElasticsearchSvc.deleteObservationDocument(deletedObservationLastNEntity.getIdentifier()); 240 } 241 } 242 243}