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