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}