001package ca.uhn.fhir.jpa.dao.search;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.RuntimeSearchParam;
024import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
025import ca.uhn.fhir.model.api.IQueryParameterType;
026import ca.uhn.fhir.rest.api.Constants;
027import ca.uhn.fhir.rest.param.DateParam;
028import ca.uhn.fhir.rest.param.NumberParam;
029import ca.uhn.fhir.rest.param.QuantityParam;
030import ca.uhn.fhir.rest.param.ReferenceParam;
031import ca.uhn.fhir.rest.param.StringParam;
032import ca.uhn.fhir.rest.param.TokenParam;
033import ca.uhn.fhir.rest.param.UriParam;
034import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
035import com.google.common.collect.Lists;
036import com.google.common.collect.Sets;
037import org.apache.commons.lang3.StringUtils;
038
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.List;
042import java.util.Set;
043
044/**
045 * Search builder for lucene/elastic for token, string, and reference parameters.
046 */
047public class ExtendedLuceneSearchBuilder {
048        public static final String EMPTY_MODIFIER = "";
049
050        /**
051         * These params have complicated semantics, or are best resolved at the JPA layer for now.
052         */
053        public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta");
054
055        /**
056         * Are any of the queries supported by our indexing?
057         */
058        public boolean isSupportsSomeOf(SearchParameterMap myParams) {
059                return
060                        myParams.getSort() != null ||
061                        myParams.getLastUpdated() != null ||
062                        myParams.entrySet().stream()
063                                .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
064                                // each and clause may have a different modifier, so split down to the ORs
065                                .flatMap(andList -> andList.getValue().stream())
066                                .flatMap(Collection::stream)
067                                .anyMatch(this::isParamTypeSupported);
068        }
069
070        /**
071         * Do we support this query param type+modifier?
072         * <p>
073         * NOTE - keep this in sync with addAndConsumeAdvancedQueryClauses() below.
074         */
075        private boolean isParamTypeSupported(IQueryParameterType param) {
076                String modifier = StringUtils.defaultString(param.getQueryParameterQualifier(), EMPTY_MODIFIER);
077                if (param instanceof TokenParam) {
078                        switch (modifier) {
079                                case Constants.PARAMQUALIFIER_TOKEN_TEXT:
080                                case "":
081                                        // we support plain token and token:text
082                                        return true;
083                                default:
084                                        return false;
085                        }
086                } else if (param instanceof StringParam) {
087                        switch (modifier) {
088                                // we support string:text, string:contains, string:exact, and unmodified string.
089                                case Constants.PARAMQUALIFIER_TOKEN_TEXT:
090                                case Constants.PARAMQUALIFIER_STRING_EXACT:
091                                case Constants.PARAMQUALIFIER_STRING_CONTAINS:
092                                case EMPTY_MODIFIER:
093                                        return true;
094                                default:
095                                        return false;
096                        }
097                } else if (param instanceof QuantityParam) {
098                        return modifier.equals(EMPTY_MODIFIER);
099
100                } else if (param instanceof ReferenceParam) {
101                        //We cannot search by chain.
102                        if (((ReferenceParam) param).getChain() != null) {
103                                return false;
104                        }
105                        switch (modifier) {
106                                case EMPTY_MODIFIER:
107                                        return true;
108                                case Constants.PARAMQUALIFIER_MDM:
109                                case Constants.PARAMQUALIFIER_NICKNAME:
110                                default:
111                                        return false;
112                        }
113                } else if (param instanceof DateParam) {
114                        if (EMPTY_MODIFIER.equals(modifier)) {
115                                return true;
116                        }
117                        return false;
118                } else if (param instanceof UriParam) {
119                        return modifier.equals(EMPTY_MODIFIER);
120
121                } else if (param instanceof NumberParam) {
122                        return modifier.equals(EMPTY_MODIFIER);
123
124                } else {
125                        return false;
126                }
127        }
128
129        public void addAndConsumeAdvancedQueryClauses(ExtendedLuceneClauseBuilder builder, String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) {
130                // copy the keys to avoid concurrent modification error
131                ArrayList<String> paramNames = compileParamNames(theParams);
132                for (String nextParam : paramNames) {
133                        if (ourUnsafeSearchParmeters.contains(nextParam)) {
134                                continue;
135                        }
136                        RuntimeSearchParam activeParam = theSearchParamRegistry.getActiveSearchParam(theResourceType, nextParam);
137                        if (activeParam == null) {
138                                // ignore magic params handled in JPA
139                                continue;
140                        }
141
142                        // NOTE - keep this in sync with isParamSupported() above.
143                        switch (activeParam.getParamType()) {
144                                case TOKEN:
145                                        List<List<IQueryParameterType>> tokenTextAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
146                                        builder.addStringTextSearch(nextParam, tokenTextAndOrTerms);
147
148                                        List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
149                                        builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
150                                        break;
151
152                                case STRING:
153                                        List<List<IQueryParameterType>> stringTextAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
154                                        builder.addStringTextSearch(nextParam, stringTextAndOrTerms);
155
156                                        List<List<IQueryParameterType>> stringExactAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_EXACT);
157                                        builder.addStringExactSearch(nextParam, stringExactAndOrTerms);
158
159                                        List<List<IQueryParameterType>> stringContainsAndOrTerms = theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS);
160                                        builder.addStringContainsSearch(nextParam, stringContainsAndOrTerms);
161
162                                        List<List<IQueryParameterType>> stringAndOrTerms = theParams.removeByNameUnmodified(nextParam);
163                                        builder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms);
164                                        break;
165
166                                case QUANTITY:
167                                        List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam);
168                                        builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
169                                        break;
170
171                                case REFERENCE:
172                                        List<List<IQueryParameterType>> referenceAndOrTerms = theParams.removeByNameUnmodified(nextParam);
173                                        builder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms);
174                                        break;
175
176                                case DATE:
177                                        List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated")
178                                                ? getLastUpdatedAndOrList(theParams) : theParams.removeByNameUnmodified(nextParam);
179                                        builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
180                                        break;
181
182                                case URI:
183                                        List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
184                                        builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
185                                        break;
186
187                                case NUMBER:
188                                        List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam);
189                                        builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
190                                        break;
191
192                                default:
193                                        // ignore unsupported param types/modifiers.  They will be processed up in SearchBuilder.
194                        }
195                }
196        }
197
198
199        private List<List<IQueryParameterType>> getLastUpdatedAndOrList(SearchParameterMap theParams) {
200                DateParam activeBound = theParams.getLastUpdated().getLowerBound() != null
201                        ? theParams.getLastUpdated().getLowerBound()
202                        : theParams.getLastUpdated().getUpperBound();
203
204                List<List<IQueryParameterType>> result = List.of( List.of(activeBound) );
205
206                // indicate parameter was processed
207                theParams.setLastUpdated(null);
208
209                return result;
210        }
211
212
213        /**
214         * Param name list is not only the params.keySet, but also the "special" parameters extracted from input
215         * (as _lastUpdated when the input myLastUpdated field is not null, etc).
216         */
217        private ArrayList<String> compileParamNames(SearchParameterMap theParams) {
218                ArrayList<String> nameList = Lists.newArrayList(theParams.keySet());
219
220                if (theParams.getLastUpdated() != null) {
221                        nameList.add("_lastUpdated");
222                }
223
224                return nameList;
225        }
226
227}