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}