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