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