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