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