001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.search;
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.entity.ResourceIndexedSearchParamDate;
026import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
027import ca.uhn.fhir.jpa.model.entity.ResourceLink;
028import ca.uhn.fhir.jpa.model.entity.ResourceTable;
029import ca.uhn.fhir.jpa.model.entity.TagDefinition;
030import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
031import ca.uhn.fhir.jpa.model.search.DateSearchIndexData;
032import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
033import ca.uhn.fhir.jpa.model.search.QuantitySearchIndexData;
034import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
035import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite;
036import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
037import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
038import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
039import ca.uhn.fhir.util.MetaUtil;
040import com.google.common.base.Strings;
041import jakarta.annotation.Nonnull;
042import org.apache.commons.lang3.ObjectUtils;
043import org.hl7.fhir.instance.model.api.IBase;
044import org.hl7.fhir.instance.model.api.IBaseCoding;
045import org.hl7.fhir.instance.model.api.IBaseResource;
046
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.HashMap;
050import java.util.List;
051import java.util.Locale;
052import java.util.Map;
053
054import static org.apache.commons.lang3.StringUtils.isNotBlank;
055
056/**
057 * Extract search params for advanced HSearch indexing.
058 * <p>
059 * This class re-uses the extracted JPA entities to build an ExtendedHSearchIndexData instance.
060 */
061public class ExtendedHSearchIndexExtractor {
062
063        private final JpaStorageSettings myJpaStorageSettings;
064        private final FhirContext myContext;
065        private final ResourceSearchParams myParams;
066        private final ISearchParamExtractor mySearchParamExtractor;
067
068        public ExtendedHSearchIndexExtractor(
069                        JpaStorageSettings theJpaStorageSettings,
070                        FhirContext theContext,
071                        ResourceSearchParams theActiveParams,
072                        ISearchParamExtractor theSearchParamExtractor) {
073                myJpaStorageSettings = theJpaStorageSettings;
074                myContext = theContext;
075                myParams = theActiveParams;
076                mySearchParamExtractor = theSearchParamExtractor;
077        }
078
079        @Nonnull
080        public ExtendedHSearchIndexData extract(
081                        IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) {
082                ExtendedHSearchIndexData retVal =
083                                new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource, theEntity);
084
085                if (myJpaStorageSettings.isStoreResourceInHSearchIndex()) {
086                        retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource));
087                }
088
089                retVal.setForcedId(theResource.getIdElement().getIdPart());
090
091                // todo add a flag ot StorageSettings to suppress this
092                extractAutocompleteTokens(theResource, retVal);
093
094                theNewParams.myStringParams.stream()
095                                .filter(nextParam -> !nextParam.isMissing())
096                                .forEach(nextParam -> retVal.addStringIndexData(nextParam.getParamName(), nextParam.getValueExact()));
097
098                theNewParams.myTokenParams.stream()
099                                .filter(nextParam -> !nextParam.isMissing())
100                                .forEach(nextParam -> retVal.addTokenIndexDataIfNotPresent(
101                                                nextParam.getParamName(), nextParam.getSystem(), nextParam.getValue()));
102
103                theNewParams.myNumberParams.stream()
104                                .filter(nextParam -> !nextParam.isMissing())
105                                .forEach(nextParam ->
106                                                retVal.addNumberIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getValue()));
107
108                theNewParams.myDateParams.stream()
109                                .filter(nextParam -> !nextParam.isMissing())
110                                .forEach(nextParam -> retVal.addDateIndexData(nextParam.getParamName(), convertDate(nextParam)));
111
112                theNewParams.myQuantityParams.stream()
113                                .filter(nextParam -> !nextParam.isMissing())
114                                .forEach(
115                                                nextParam -> retVal.addQuantityIndexData(nextParam.getParamName(), convertQuantity(nextParam)));
116
117                theNewParams.myUriParams.stream()
118                                .filter(nextParam -> !nextParam.isMissing())
119                                .forEach(nextParam -> retVal.addUriIndexData(nextParam.getParamName(), nextParam.getUri()));
120
121                theEntity.getTags().forEach(tag -> {
122                        TagDefinition td = tag.getTag();
123
124                        IBaseCoding coding = (IBaseCoding) myContext.getVersion().newCodingDt();
125                        coding.setVersion(td.getVersion());
126                        coding.setDisplay(td.getDisplay());
127                        coding.setCode(td.getCode());
128                        coding.setSystem(td.getSystem());
129                        coding.setUserSelected(ObjectUtils.defaultIfNull(td.getUserSelected(), false));
130                        switch (td.getTagType()) {
131                                case TAG:
132                                        retVal.addTokenIndexData("_tag", coding);
133                                        break;
134                                case PROFILE:
135                                        retVal.addUriIndexData("_profile", coding.getCode());
136                                        break;
137                                case SECURITY_LABEL:
138                                        retVal.addTokenIndexData("_security", coding);
139                                        break;
140                        }
141                });
142
143                String source = MetaUtil.getSource(myContext, theResource.getMeta());
144                if (isNotBlank(source)) {
145                        retVal.addUriIndexData("_source", source);
146                }
147
148                theNewParams.myCompositeParams.forEach(nextParam ->
149                                retVal.addCompositeIndexData(nextParam.getSearchParamName(), buildCompositeIndexData(nextParam)));
150
151                if (theEntity.getUpdated() != null && !theEntity.getUpdated().isEmpty()) {
152                        int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theEntity.getUpdatedDate())
153                                        .intValue();
154                        retVal.addDateIndexData(
155                                        "_lastUpdated", theEntity.getUpdatedDate(), ordinal, theEntity.getUpdatedDate(), ordinal);
156                }
157
158                if (!theNewParams.myLinks.isEmpty()) {
159                        // awkwardly, links are indexed by jsonpath, not by search param.
160                        // so we re-build the linkage.
161                        Map<String, List<String>> linkPathToParamName = new HashMap<>();
162                        for (String nextParamName : theNewParams.getPopulatedResourceLinkParameters()) {
163                                RuntimeSearchParam sp = myParams.get(nextParamName);
164                                List<String> pathsSplit = sp.getPathsSplit();
165                                for (String nextPath : pathsSplit) {
166                                        // we want case-insensitive matching
167                                        nextPath = nextPath.toLowerCase(Locale.ROOT);
168
169                                        linkPathToParamName
170                                                        .computeIfAbsent(nextPath, (p) -> new ArrayList<>())
171                                                        .add(nextParamName);
172                                }
173                        }
174
175                        for (ResourceLink nextLink : theNewParams.getResourceLinks()) {
176                                String insensitivePath = nextLink.getSourcePath().toLowerCase(Locale.ROOT);
177                                List<String> paramNames = linkPathToParamName.getOrDefault(insensitivePath, Collections.emptyList());
178                                for (String nextParamName : paramNames) {
179                                        String qualifiedTargetResourceId = "";
180                                        // Consider 2 cases for references
181                                        // Case 1: Resource Type and Resource ID is known
182                                        // Case 2: Resource is unknown and referred by canonical url reference
183                                        if (!Strings.isNullOrEmpty(nextLink.getTargetResourceId())) {
184                                                qualifiedTargetResourceId =
185                                                                nextLink.getTargetResourceType() + "/" + nextLink.getTargetResourceId();
186                                        } else if (!Strings.isNullOrEmpty(nextLink.getTargetResourceUrl())) {
187                                                qualifiedTargetResourceId = nextLink.getTargetResourceUrl();
188                                        }
189                                        retVal.addResourceLinkIndexData(nextParamName, qualifiedTargetResourceId);
190                                }
191                        }
192                }
193
194                return retVal;
195        }
196
197        @Nonnull
198        private CompositeSearchIndexData buildCompositeIndexData(
199                        ResourceIndexedSearchParamComposite theSearchParamComposite) {
200                return new HSearchCompositeSearchIndexDataImpl(theSearchParamComposite);
201        }
202
203        /**
204         * Re-extract token parameters so we can distinguish
205         */
206        private void extractAutocompleteTokens(IBaseResource theResource, ExtendedHSearchIndexData theRetVal) {
207                // we need to re-index token params to match up display with codes.
208                myParams.values().stream()
209                                .filter(p -> p.getParamType() == RestSearchParameterTypeEnum.TOKEN)
210                                // TODO it would be nice to reuse TokenExtractor
211                                .forEach(p -> mySearchParamExtractor
212                                                .extractValues(p.getPath(), theResource)
213                                                .forEach(nextValue -> indexTokenValue(theRetVal, p, nextValue)));
214        }
215
216        private void indexTokenValue(ExtendedHSearchIndexData theRetVal, RuntimeSearchParam p, IBase nextValue) {
217                String nextType = mySearchParamExtractor.toRootTypeName(nextValue);
218                String spName = p.getName();
219                switch (nextType) {
220                        case "CodeableConcept":
221                                addToken_CodeableConcept(theRetVal, spName, nextValue);
222                                break;
223                        case "Coding":
224                                addToken_Coding(theRetVal, spName, (IBaseCoding) nextValue);
225                                break;
226                                // TODO share this with TokenExtractor and introduce a ITokenIndexer interface.
227                                // Ignore unknown types for now.
228                                // This is just for autocomplete, and we are focused on Observation.code, category, combo-code, etc.
229                                //                                      case "Identifier":
230                                //                                              mySearchParamExtractor.addToken_Identifier(myResourceTypeName, params, searchParam, value);
231                                //                                              break;
232                                //                                      case "ContactPoint":
233                                //                                              mySearchParamExtractor.addToken_ContactPoint(myResourceTypeName, params, searchParam, value);
234                                //                                              break;
235                        default:
236                                break;
237                }
238        }
239
240        private void addToken_CodeableConcept(ExtendedHSearchIndexData theRetVal, String theSpName, IBase theValue) {
241                List<IBase> codings = mySearchParamExtractor.getCodingsFromCodeableConcept(theValue);
242                for (IBase nextCoding : codings) {
243                        addToken_Coding(theRetVal, theSpName, (IBaseCoding) nextCoding);
244                }
245        }
246
247        private void addToken_Coding(ExtendedHSearchIndexData theRetVal, String theSpName, IBaseCoding theNextValue) {
248                theRetVal.addTokenIndexData(theSpName, theNextValue);
249        }
250
251        @Nonnull
252        public static DateSearchIndexData convertDate(ResourceIndexedSearchParamDate nextParam) {
253                return new DateSearchIndexData(
254                                nextParam.getValueLow(),
255                                nextParam.getValueLowDateOrdinal(),
256                                nextParam.getValueHigh(),
257                                nextParam.getValueHighDateOrdinal());
258        }
259
260        @Nonnull
261        public static QuantitySearchIndexData convertQuantity(ResourceIndexedSearchParamQuantity nextParam) {
262                return new QuantitySearchIndexData(
263                                nextParam.getUnits(),
264                                nextParam.getSystem(),
265                                nextParam.getValue().doubleValue());
266        }
267}