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.RuntimeSearchParam;
023import ca.uhn.fhir.jpa.model.search.ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams;
024import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
025import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
026import ca.uhn.fhir.model.api.IQueryParameterType;
027import ca.uhn.fhir.rest.api.Constants;
028import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
029import ca.uhn.fhir.rest.param.CompositeParam;
030import ca.uhn.fhir.rest.param.DateParam;
031import ca.uhn.fhir.rest.param.DateRangeParam;
032import ca.uhn.fhir.rest.param.NumberParam;
033import ca.uhn.fhir.rest.param.QuantityParam;
034import ca.uhn.fhir.rest.param.ReferenceParam;
035import ca.uhn.fhir.rest.param.StringParam;
036import ca.uhn.fhir.rest.param.TokenParam;
037import ca.uhn.fhir.rest.param.UriParam;
038import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
039import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
040import com.google.common.collect.Lists;
041import com.google.common.collect.Sets;
042import org.apache.commons.collections4.CollectionUtils;
043import org.apache.commons.lang3.BooleanUtils;
044import org.apache.commons.lang3.StringUtils;
045
046import java.util.ArrayList;
047import java.util.Collection;
048import java.util.List;
049import java.util.Set;
050import java.util.stream.Collectors;
051
052import static ca.uhn.fhir.rest.api.Constants.PARAMQUALIFIER_MISSING;
053
054/**
055 * Search builder for HSearch for token, string, and reference parameters.
056 */
057public class ExtendedHSearchSearchBuilder {
058        public static final String EMPTY_MODIFIER = "";
059
060        /**
061         * These params have complicated semantics, or are best resolved at the JPA layer for now.
062         */
063        public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta", "_count");
064
065        /**
066         * Determine if ExtendedHibernateSearchBuilder can support this parameter
067         * @param theParamName param name
068         * @param theActiveParamsForResourceType active search parameters for the desired resource type
069         * @return whether or not this search parameter is supported in hibernate
070         */
071        public boolean illegalForHibernateSearch(String theParamName, ResourceSearchParams theActiveParamsForResourceType) {
072                if (theActiveParamsForResourceType == null) {
073                        return true;
074                }
075                if (ourUnsafeSearchParmeters.contains(theParamName)) {
076                        return true;
077                }
078                if (!theActiveParamsForResourceType.containsParamName(theParamName)) {
079                        return true;
080                }
081                return false;
082        }
083
084        /**
085         * By default, do not use Hibernate Search.
086         * If a Search Parameter is supported by hibernate search,
087         * Are any of the queries supported by our indexing?
088         * -
089         * If not, do not use hibernate, because the results will
090         * be inaccurate and wrong.
091         */
092        public boolean canUseHibernateSearch(
093                        String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) {
094                boolean canUseHibernate = false;
095
096                ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(
097                                theResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
098                for (String paramName : theParams.keySet()) {
099                        // special SearchParam handling:
100                        // _lastUpdated
101                        if (theParams.getLastUpdated() != null) {
102                                canUseHibernate = !illegalForHibernateSearch(Constants.PARAM_LASTUPDATED, resourceActiveSearchParams);
103                                if (!canUseHibernate) {
104                                        return false;
105                                }
106                        }
107
108                        // is this parameter supported?
109                        if (illegalForHibernateSearch(paramName, resourceActiveSearchParams)) {
110                                canUseHibernate = false;
111                        } else {
112                                // are the parameter values supported?
113                                canUseHibernate =
114                                                theParams.get(paramName).stream()
115                                                                .flatMap(Collection::stream)
116                                                                .collect(Collectors.toList())
117                                                                .stream()
118                                                                .anyMatch(this::isParamTypeSupported);
119                        }
120
121                        // if not supported, don't use
122                        if (!canUseHibernate) {
123                                return false;
124                        }
125                }
126
127                return canUseHibernate;
128        }
129
130        /**
131         * Are all the queries supported by our indexing?
132         */
133        public boolean isSupportsAllOf(SearchParameterMap myParams) {
134                return CollectionUtils.isEmpty(myParams.getRevIncludes())
135                                && // ???
136                                CollectionUtils.isEmpty(myParams.getIncludes())
137                                && // ???
138                                myParams.getEverythingMode() == null
139                                && // ???
140                                BooleanUtils.isFalse(myParams.isDeleteExpunge())
141                                && // ???
142
143                                // not yet supported in HSearch
144                                myParams.getNearDistanceParam() == null
145                                && // ???
146
147                                // not yet supported in HSearch
148                                myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
149                                && supportsLastUpdated(myParams)
150                                && // ???
151                                myParams.entrySet().stream()
152                                                .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
153                                                // each and clause may have a different modifier, so split down to the ORs
154                                                .flatMap(andList -> andList.getValue().stream())
155                                                .flatMap(Collection::stream)
156                                                .allMatch(this::isParamTypeSupported);
157        }
158
159        private boolean supportsLastUpdated(SearchParameterMap theMap) {
160                if (theMap.getLastUpdated() == null || theMap.getLastUpdated().isEmpty()) {
161                        return true;
162                }
163
164                DateRangeParam lastUpdated = theMap.getLastUpdated();
165
166                return lastUpdated.getLowerBound() != null
167                                && isParamTypeSupported(lastUpdated.getLowerBound())
168                                && lastUpdated.getUpperBound() != null
169                                && isParamTypeSupported(lastUpdated.getUpperBound());
170        }
171
172        /**
173         * Do we support this query param type+modifier?
174         * <p>
175         * NOTE - keep this in sync with addAndConsumeAdvancedQueryClauses() below.
176         */
177        private boolean isParamTypeSupported(IQueryParameterType param) {
178                String modifier = StringUtils.defaultString(param.getQueryParameterQualifier(), EMPTY_MODIFIER);
179                if (param instanceof TokenParam) {
180                        switch (modifier) {
181                                case Constants.PARAMQUALIFIER_TOKEN_TEXT:
182                                case "":
183                                        // we support plain token and token:text
184                                        return true;
185                                default:
186                                        return false;
187                        }
188                } else if (param instanceof StringParam) {
189                        switch (modifier) {
190                                        // we support string:text, string:contains, string:exact, and unmodified string.
191                                case Constants.PARAMQUALIFIER_STRING_TEXT:
192                                case Constants.PARAMQUALIFIER_STRING_EXACT:
193                                case Constants.PARAMQUALIFIER_STRING_CONTAINS:
194                                case EMPTY_MODIFIER:
195                                        return true;
196                                default:
197                                        return false;
198                        }
199                } else if (param instanceof QuantityParam) {
200                        return modifier.equals(EMPTY_MODIFIER);
201
202                } else if (param instanceof CompositeParam) {
203                        switch (modifier) {
204                                case PARAMQUALIFIER_MISSING:
205                                        return false;
206                                default:
207                                        return true;
208                        }
209
210                } else if (param instanceof ReferenceParam) {
211                        // We cannot search by chain.
212                        if (((ReferenceParam) param).getChain() != null) {
213                                return false;
214                        }
215                        switch (modifier) {
216                                case EMPTY_MODIFIER:
217                                        return true;
218                                case Constants.PARAMQUALIFIER_MDM:
219                                case Constants.PARAMQUALIFIER_NICKNAME:
220                                default:
221                                        return false;
222                        }
223                } else if (param instanceof DateParam) {
224                        return modifier.equals(EMPTY_MODIFIER);
225
226                } else if (param instanceof UriParam) {
227                        return modifier.equals(EMPTY_MODIFIER);
228
229                } else if (param instanceof NumberParam) {
230                        return modifier.equals(EMPTY_MODIFIER);
231
232                } else {
233                        return false;
234                }
235        }
236
237        public void addAndConsumeAdvancedQueryClauses(
238                        ExtendedHSearchClauseBuilder theBuilder,
239                        ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams theMethodParams) {
240                SearchParameterMap searchParameterMap = theMethodParams.getSearchParameterMap();
241                String resourceType = theMethodParams.getResourceType();
242                ISearchParamRegistry searchParamRegistry = theMethodParams.getSearchParamRegistry();
243
244                // copy the keys to avoid concurrent modification error
245                ArrayList<String> paramNames = compileParamNames(searchParameterMap);
246                ResourceSearchParams activeSearchParams = searchParamRegistry.getActiveSearchParams(
247                                resourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
248                for (String nextParam : paramNames) {
249                        if (illegalForHibernateSearch(nextParam, activeSearchParams)) {
250                                // ignore magic params handled in JPA
251                                continue;
252                        }
253                        RuntimeSearchParam activeParam = activeSearchParams.get(nextParam);
254
255                        // NOTE - keep this in sync with isParamSupported() above.
256                        switch (activeParam.getParamType()) {
257                                case TOKEN:
258                                        List<List<IQueryParameterType>> tokenTextAndOrTerms =
259                                                        searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
260                                        theBuilder.addStringTextSearch(nextParam, tokenTextAndOrTerms);
261
262                                        List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms =
263                                                        searchParameterMap.removeByNameUnmodified(nextParam);
264                                        theBuilder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
265                                        break;
266
267                                case STRING:
268                                        List<List<IQueryParameterType>> stringTextAndOrTerms =
269                                                        searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
270                                        theBuilder.addStringTextSearch(nextParam, stringTextAndOrTerms);
271
272                                        List<List<IQueryParameterType>> stringExactAndOrTerms = searchParameterMap.removeByNameAndModifier(
273                                                        nextParam, Constants.PARAMQUALIFIER_STRING_EXACT);
274                                        theBuilder.addStringExactSearch(nextParam, stringExactAndOrTerms);
275
276                                        List<List<IQueryParameterType>> stringContainsAndOrTerms =
277                                                        searchParameterMap.removeByNameAndModifier(
278                                                                        nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS);
279                                        theBuilder.addStringContainsSearch(nextParam, stringContainsAndOrTerms);
280
281                                        List<List<IQueryParameterType>> stringAndOrTerms =
282                                                        searchParameterMap.removeByNameUnmodified(nextParam);
283                                        theBuilder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms);
284                                        break;
285
286                                case QUANTITY:
287                                        List<List<IQueryParameterType>> quantityAndOrTerms =
288                                                        searchParameterMap.removeByNameUnmodified(nextParam);
289                                        theBuilder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
290                                        break;
291
292                                case REFERENCE:
293                                        List<List<IQueryParameterType>> referenceAndOrTerms =
294                                                        searchParameterMap.removeByNameUnmodified(nextParam);
295                                        theBuilder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms);
296                                        break;
297
298                                case DATE:
299                                        List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated")
300                                                        ? getLastUpdatedAndOrList(searchParameterMap)
301                                                        : searchParameterMap.removeByNameUnmodified(nextParam);
302                                        theBuilder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
303                                        break;
304
305                                case COMPOSITE:
306                                        List<List<IQueryParameterType>> compositeAndOrTerms =
307                                                        searchParameterMap.removeByNameUnmodified(nextParam);
308                                        // RuntimeSearchParam only points to the subs by reference.  Resolve here while we have
309                                        // ISearchParamRegistry
310                                        List<RuntimeSearchParam> subSearchParams =
311                                                        JpaParamUtil.resolveCompositeComponentsDeclaredOrder(searchParamRegistry, activeParam);
312                                        theBuilder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms);
313                                        break;
314
315                                case URI:
316                                        List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms =
317                                                        searchParameterMap.removeByNameUnmodified(nextParam);
318                                        theBuilder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
319                                        break;
320
321                                case NUMBER:
322                                        List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = searchParameterMap.remove(nextParam);
323                                        theBuilder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
324                                        break;
325
326                                default:
327                                        // ignore unsupported param types/modifiers.  They will be processed up in SearchBuilder.
328                        }
329                }
330        }
331
332        private List<List<IQueryParameterType>> getLastUpdatedAndOrList(SearchParameterMap theParams) {
333                DateParam activeBound = theParams.getLastUpdated().getLowerBound() != null
334                                ? theParams.getLastUpdated().getLowerBound()
335                                : theParams.getLastUpdated().getUpperBound();
336
337                List<List<IQueryParameterType>> result = List.of(List.of(activeBound));
338
339                // indicate parameter was processed
340                theParams.setLastUpdated(null);
341
342                return result;
343        }
344
345        /**
346         * Param name list is not only the params.keySet, but also the "special" parameters extracted from input
347         * (as _lastUpdated when the input myLastUpdated field is not null, etc).
348         */
349        private ArrayList<String> compileParamNames(SearchParameterMap theParams) {
350                ArrayList<String> nameList = Lists.newArrayList(theParams.keySet());
351
352                if (theParams.getLastUpdated() != null) {
353                        nameList.add("_lastUpdated");
354                }
355
356                return nameList;
357        }
358}