001/*-
002 * #%L
003 * HAPI FHIR Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2023 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.searchparam.matcher;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.context.support.ConceptValidationOptions;
027import ca.uhn.fhir.context.support.IValidationSupport;
028import ca.uhn.fhir.context.support.ValidationSupportContext;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
031import ca.uhn.fhir.jpa.model.entity.StorageSettings;
032import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
033import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
034import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
035import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
036import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
037import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
038import ca.uhn.fhir.model.api.IQueryParameterType;
039import ca.uhn.fhir.rest.api.Constants;
040import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
041import ca.uhn.fhir.rest.api.server.RequestDetails;
042import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
043import ca.uhn.fhir.rest.param.ParamPrefixEnum;
044import ca.uhn.fhir.rest.param.ReferenceParam;
045import ca.uhn.fhir.rest.param.StringParam;
046import ca.uhn.fhir.rest.param.TokenParam;
047import ca.uhn.fhir.rest.param.TokenParamModifier;
048import ca.uhn.fhir.rest.param.UriParam;
049import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
050import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
051import ca.uhn.fhir.util.MetaUtil;
052import ca.uhn.fhir.util.UrlUtil;
053import com.google.common.collect.Sets;
054import org.apache.commons.lang3.Validate;
055import org.hl7.fhir.dstu3.model.Location;
056import org.hl7.fhir.instance.model.api.IAnyResource;
057import org.hl7.fhir.instance.model.api.IBaseCoding;
058import org.hl7.fhir.instance.model.api.IBaseResource;
059import org.hl7.fhir.instance.model.api.IIdType;
060import org.hl7.fhir.instance.model.api.IPrimitiveType;
061import org.slf4j.LoggerFactory;
062import org.springframework.beans.BeansException;
063import org.springframework.beans.factory.annotation.Autowired;
064import org.springframework.context.ApplicationContext;
065
066import javax.annotation.Nonnull;
067import javax.annotation.Nullable;
068import java.util.List;
069import java.util.Map;
070import java.util.Set;
071import java.util.stream.Collectors;
072
073import static org.apache.commons.lang3.StringUtils.isBlank;
074import static org.apache.commons.lang3.StringUtils.isNotBlank;
075
076public class InMemoryResourceMatcher {
077
078        public static final Set<String> UNSUPPORTED_PARAMETER_NAMES = Sets.newHashSet(Constants.PARAM_HAS);
079        private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(InMemoryResourceMatcher.class);
080        @Autowired
081        ApplicationContext myApplicationContext;
082        @Autowired
083        ISearchParamRegistry mySearchParamRegistry;
084        @Autowired
085        StorageSettings myStorageSettings;
086        @Autowired
087        FhirContext myFhirContext;
088        @Autowired
089        SearchParamExtractorService mySearchParamExtractorService;
090        @Autowired
091        IndexedSearchParamExtractor myIndexedSearchParamExtractor;
092        @Autowired
093        private MatchUrlService myMatchUrlService;
094        private ValidationSupportInitializationState validationSupportState = ValidationSupportInitializationState.NOT_INITIALIZED;
095        private IValidationSupport myValidationSupport = null;
096        public InMemoryResourceMatcher() {
097        }
098
099        /**
100         * Lazy loads a {@link IValidationSupport} implementation just-in-time.
101         * If no suitable bean is available, or if a {@link ca.uhn.fhir.context.ConfigurationException} is thrown, matching
102         * can proceed, but the qualifiers that depend on the validation support will be disabled.
103         *
104         * @return A bean implementing {@link IValidationSupport} if one is available, otherwise null
105         */
106        private IValidationSupport getValidationSupportOrNull() {
107                if (validationSupportState == ValidationSupportInitializationState.NOT_INITIALIZED) {
108                        try {
109                                myValidationSupport = myApplicationContext.getBean(IValidationSupport.class);
110                                validationSupportState = ValidationSupportInitializationState.INITIALIZED;
111                        } catch (BeansException | ConfigurationException ignore) {
112                                // We couldn't get a validation support bean, and we don't want to waste cycles trying again
113                                ourLog.warn(Msg.code(2100) + "No bean satisfying IValidationSupport could be initialized. Qualifiers dependent on IValidationSupport will not be supported.");
114                                validationSupportState = ValidationSupportInitializationState.FAILED;
115                        }
116                }
117                return myValidationSupport;
118        }
119
120        /**
121         * @deprecated Use {@link #match(String, IBaseResource, ResourceIndexedSearchParams, RequestDetails)}
122         */
123        @Deprecated
124        public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, @Nullable ResourceIndexedSearchParams theIndexedSearchParams) {
125                return match(theCriteria, theResource, theIndexedSearchParams, null);
126        }
127
128
129        /**
130                 * This method is called in two different scenarios.  With a null theResource, it determines whether database matching might be required.
131                 * Otherwise, it tries to perform the match in-memory, returning UNSUPPORTED if it's not possible.
132                 * <p>
133                 * Note that there will be cases where it returns UNSUPPORTED with a null resource, but when a non-null resource it returns supported and no match.
134                 * This is because an earlier parameter may be matchable in-memory in which case processing stops and we never get to the parameter
135                 * that would have required a database call.
136                 *
137                 * @param theIndexedSearchParams If the search params have already been calculated for the given resource,
138                 *                               they can be passed in. Passing in {@literal null} is also fine, in which
139                 *                               case they will be calculated for the resource. It can be preferable to
140                 *                               pass in {@literal null} unless you already actually had to calculate the
141                 *                               indexes for another reason, since we can be efficient here and only calculate
142                 *                               the params that are actually relevant for the given search expression.
143                 */
144        public InMemoryMatchResult match(String theCriteria, IBaseResource theResource, @Nullable ResourceIndexedSearchParams theIndexedSearchParams, RequestDetails theRequestDetails) {
145                RuntimeResourceDefinition resourceDefinition;
146                if (theResource == null) {
147                        Validate.isTrue(!theCriteria.startsWith("?"), "Invalid match URL format (must match \"[resourceType]?[params]\")");
148                        Validate.isTrue(theCriteria.contains("?"), "Invalid match URL format (must match \"[resourceType]?[params]\")");
149                        resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theCriteria);
150                } else {
151                        resourceDefinition = myFhirContext.getResourceDefinition(theResource);
152                }
153                SearchParameterMap searchParameterMap;
154                try {
155                        searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, resourceDefinition);
156                } catch (UnsupportedOperationException e) {
157                        return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.PARSE_FAIL);
158                }
159                searchParameterMap.clean();
160
161                ResourceIndexedSearchParams relevantSearchParams = null;
162                if (theIndexedSearchParams != null) {
163                        relevantSearchParams = theIndexedSearchParams;
164                } else if (theResource != null) {
165                        // Don't index search params we don't actully need for the given criteria
166                        ISearchParamExtractor.ISearchParamFilter filter = theSearchParams -> theSearchParams
167                                .stream()
168                                .filter(t -> searchParameterMap.containsKey(t.getName()))
169                                .collect(Collectors.toList());
170                        relevantSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequestDetails, filter);
171                }
172
173                return match(searchParameterMap, theResource, resourceDefinition, relevantSearchParams);
174        }
175
176        /**
177         * @param theCriteria
178         * @return result.supported() will be true if theCriteria can be evaluated in-memory
179         */
180        public InMemoryMatchResult canBeEvaluatedInMemory(String theCriteria) {
181                return match(theCriteria, null, null, null);
182        }
183
184        /**
185         * @param theSearchParameterMap
186         * @param theResourceDefinition
187         * @return result.supported() will be true if theSearchParameterMap can be evaluated in-memory
188         */
189        public InMemoryMatchResult canBeEvaluatedInMemory(SearchParameterMap theSearchParameterMap, RuntimeResourceDefinition theResourceDefinition) {
190                return match(theSearchParameterMap, null, theResourceDefinition, null);
191        }
192
193        @Nonnull
194        public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
195                if (theSearchParameterMap.getLastUpdated() != null) {
196                        return InMemoryMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, InMemoryMatchResult.STANDARD_PARAMETER);
197                }
198                if (theSearchParameterMap.containsKey(Location.SP_NEAR)) {
199                        return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.LOCATION_NEAR);
200                }
201
202                for (Map.Entry<String, List<List<IQueryParameterType>>> entry : theSearchParameterMap.entrySet()) {
203                        String theParamName = entry.getKey();
204                        List<List<IQueryParameterType>> theAndOrParams = entry.getValue();
205                        InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theResource, theSearchParams);
206                        if (!result.matched()) {
207                                return result;
208                        }
209                }
210                return InMemoryMatchResult.successfulMatch();
211        }
212
213        // This method is modelled from SearchBuilder.searchForIdsWithAndOr()
214        private InMemoryMatchResult matchIdsWithAndOr(String theParamName, List<List<IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) {
215                if (theAndOrParams.isEmpty()) {
216                        return InMemoryMatchResult.successfulMatch();
217                }
218
219                String resourceName = theResourceDefinition.getName();
220                RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
221                InMemoryMatchResult checkUnsupportedResult = checkForUnsupportedParameters(theParamName, paramDef, theAndOrParams);
222                if (!checkUnsupportedResult.supported()) {
223                        return checkUnsupportedResult;
224                }
225
226                switch (theParamName) {
227                        case IAnyResource.SP_RES_ID:
228                                return InMemoryMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource));
229                        case Constants.PARAM_SOURCE:
230                                return InMemoryMatchResult.fromBoolean(matchSourcesAndOr(theAndOrParams, theResource));
231                        case Constants.PARAM_TAG:
232                                return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, true));
233                        case Constants.PARAM_SECURITY:
234                                return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, false));
235                        case Constants.PARAM_PROFILE:
236                                return InMemoryMatchResult.fromBoolean(matchProfilesAndOr(theAndOrParams, theResource));
237                        default:
238                                return matchResourceParam(myStorageSettings, theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
239                }
240        }
241
242        private InMemoryMatchResult checkForUnsupportedParameters(String theParamName, RuntimeSearchParam theParamDef, List<List<IQueryParameterType>> theAndOrParams) {
243
244                if (UNSUPPORTED_PARAMETER_NAMES.contains(theParamName)) {
245                        return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM);
246                }
247
248                for (List<IQueryParameterType> orParams : theAndOrParams) {
249                        // The list should never be empty, but better safe than sorry
250                        if (orParams.size() > 0) {
251                                // The params in each OR list all share the same qualifier, prefix, etc., so we only need to check the first one
252                                InMemoryMatchResult checkUnsupportedResult = checkOneParameterForUnsupportedModifiers(theParamName, theParamDef, orParams.get(0));
253                                if (!checkUnsupportedResult.supported()) {
254                                        return checkUnsupportedResult;
255                                }
256                        }
257                }
258
259                return InMemoryMatchResult.successfulMatch();
260        }
261
262        private InMemoryMatchResult checkOneParameterForUnsupportedModifiers(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
263                // Assume we're ok until we find evidence we aren't
264                InMemoryMatchResult checkUnsupportedResult = InMemoryMatchResult.successfulMatch();
265
266                if (hasChain(theParam)) {
267                        checkUnsupportedResult = InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + "." + ((ReferenceParam) theParam).getChain(), InMemoryMatchResult.CHAIN);
268                }
269
270                if (checkUnsupportedResult.supported()) {
271                        checkUnsupportedResult = checkUnsupportedQualifiers(theParamName, theParamDef, theParam);
272                }
273
274                if (checkUnsupportedResult.supported()) {
275                        checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, theParamDef, theParam);
276                }
277
278                return checkUnsupportedResult;
279        }
280
281        private boolean matchProfilesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
282                if (theResource == null) {
283                        return true;
284                }
285                return theAndOrParams.stream().allMatch(nextAnd -> matchProfilesOr(nextAnd, theResource));
286        }
287
288        private boolean matchProfilesOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) {
289                return theOrParams.stream().anyMatch(param -> matchProfile(param, theResource));
290        }
291
292        private boolean matchProfile(IQueryParameterType theProfileParam, IBaseResource theResource) {
293                UriParam paramProfile = new UriParam(theProfileParam.getValueAsQueryToken(myFhirContext));
294
295                String paramProfileValue = paramProfile.getValue();
296                if (isBlank(paramProfileValue)) {
297                        return false;
298                } else {
299                        return theResource.getMeta().getProfile().stream()
300                                .map(IPrimitiveType::getValueAsString)
301                                .anyMatch(profileValue -> profileValue != null && profileValue.equals(paramProfileValue));
302                }
303        }
304
305        private boolean matchSourcesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
306                if (theResource == null) {
307                        return true;
308                }
309                return theAndOrParams.stream().allMatch(nextAnd -> matchSourcesOr(nextAnd, theResource));
310        }
311
312        private boolean matchSourcesOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) {
313                return theOrParams.stream().anyMatch(param -> matchSource(param, theResource));
314        }
315
316        private boolean matchSource(IQueryParameterType theSourceParam, IBaseResource theResource) {
317                SourceParam paramSource = new SourceParam(theSourceParam.getValueAsQueryToken(myFhirContext));
318                SourceParam resourceSource = new SourceParam(MetaUtil.getSource(myFhirContext, theResource.getMeta()));
319                boolean matches = true;
320                if (paramSource.getSourceUri() != null) {
321                        matches = paramSource.getSourceUri().equals(resourceSource.getSourceUri());
322                }
323                if (paramSource.getRequestId() != null) {
324                        matches &= paramSource.getRequestId().equals(resourceSource.getRequestId());
325                }
326                return matches;
327        }
328
329        private boolean matchTagsOrSecurityAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource, boolean theTag) {
330                if (theResource == null) {
331                        return true;
332                }
333                return theAndOrParams.stream().allMatch(nextAnd -> matchTagsOrSecurityOr(nextAnd, theResource, theTag));
334        }
335
336        private boolean matchTagsOrSecurityOr(List<IQueryParameterType> theOrParams, IBaseResource theResource, boolean theTag) {
337                return theOrParams.stream().anyMatch(param -> matchTagOrSecurity(param, theResource, theTag));
338        }
339
340        private boolean matchTagOrSecurity(IQueryParameterType theParam, IBaseResource theResource, boolean theTag) {
341                TokenParam param = (TokenParam) theParam;
342
343                List<? extends IBaseCoding> list;
344                if (theTag) {
345                        list = theResource.getMeta().getTag();
346                } else {
347                        list = theResource.getMeta().getSecurity();
348                }
349                boolean haveMatch = false;
350                boolean haveCandidate = false;
351                for (IBaseCoding next : list) {
352                        if (param.getSystem() == null && param.getValue() == null) {
353                                continue;
354                        }
355                        haveCandidate = true;
356                        if (isNotBlank(param.getSystem())) {
357                                if (!param.getSystem().equals(next.getSystem())) {
358                                        continue;
359                                }
360                        }
361                        if (isNotBlank(param.getValue())) {
362                                if (!param.getValue().equals(next.getCode())) {
363                                        continue;
364                                }
365                        }
366                        haveMatch = true;
367                        break;
368                }
369
370                if (param.getModifier() == TokenParamModifier.NOT) {
371                        haveMatch = !haveMatch;
372                }
373
374                return haveMatch && haveCandidate;
375        }
376
377        private boolean matchIdsAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) {
378                if (theResource == null) {
379                        return true;
380                }
381                return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource));
382        }
383
384        private boolean matchIdsOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) {
385                return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam) param).getValue(), theResource.getIdElement()));
386        }
387
388        private boolean matchId(String theValue, IIdType theId) {
389                return theValue.equals(theId.getValue()) || theValue.equals(theId.getIdPart());
390        }
391
392        private InMemoryMatchResult matchResourceParam(StorageSettings theStorageSettings, String theParamName, List<List<IQueryParameterType>> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) {
393                if (theParamDef != null) {
394                        switch (theParamDef.getParamType()) {
395                                case QUANTITY:
396                                case TOKEN:
397                                case STRING:
398                                case NUMBER:
399                                case URI:
400                                case DATE:
401                                case REFERENCE:
402                                        if (theSearchParams == null) {
403                                                return InMemoryMatchResult.successfulMatch();
404                                        } else {
405                                                return InMemoryMatchResult.fromBoolean(theAndOrParams.stream().allMatch(nextAnd -> matchParams(theStorageSettings, theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
406                                        }
407                                case COMPOSITE:
408                                case HAS:
409                                case SPECIAL:
410                                default:
411                                        return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM);
412                        }
413                } else {
414                        if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
415                                return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM);
416                        } else {
417                                throw new InvalidRequestException(Msg.code(509) + "Unknown search parameter " + theParamName + " for resource type " + theResourceName);
418                        }
419                }
420        }
421
422        private boolean matchParams(StorageSettings theStorageSettings, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList, ResourceIndexedSearchParams theSearchParams) {
423
424                boolean isNegativeTest = isNegative(theParamDef, theOrList);
425                // negative tests like :not and :not-in must not match any or-clause, so we invert the quantifier.
426                if (isNegativeTest) {
427                        return theOrList.stream().allMatch(token -> matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token));
428                } else {
429                        return theOrList.stream().anyMatch(token -> matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token));
430                }
431        }
432
433        /**
434         * Some modifiers are negative, and must match NONE of their or-list
435         */
436        private boolean isNegative(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList) {
437                if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) {
438                        TokenParam tokenParam = (TokenParam) theOrList.get(0);
439                        TokenParamModifier modifier = tokenParam.getModifier();
440                        return modifier != null && modifier.isNegative();
441                } else {
442                        return false;
443                }
444
445        }
446
447        private boolean matchParam(StorageSettings theStorageSettings, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, IQueryParameterType theToken) {
448                if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) {
449                        return matchTokenParam(theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, (TokenParam) theToken);
450                } else {
451                        return theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theToken);
452                }
453        }
454
455        /**
456         * Checks whether a query parameter of type token matches one of the search parameters of an in-memory resource.
457         * The :not modifier is supported.
458         * The :in and :not-in qualifiers are supported only if a bean implementing IValidationSupport is available.
459         * Any other qualifier will be ignored and the match will be treated as unqualified.
460         *
461         * @param theStorageSettings a model configuration
462         * @param theResourceName    the name of the resource type being matched
463         * @param theParamName       the name of the parameter
464         * @param theParamDef        the definition of the search parameter
465         * @param theSearchParams    the search parameters derived from the target resource
466         * @param theQueryParam      the query parameter to compare with theSearchParams
467         * @return true if theQueryParam matches the collection of theSearchParams, otherwise false
468         */
469        private boolean matchTokenParam(StorageSettings theStorageSettings, String theResourceName, String theParamName, RuntimeSearchParam theParamDef, ResourceIndexedSearchParams theSearchParams, TokenParam theQueryParam) {
470                if (theQueryParam.getModifier() != null) {
471                        switch (theQueryParam.getModifier()) {
472                                case IN:
473                                        return theSearchParams.myTokenParams.stream()
474                                                .filter(t -> t.getParamName().equals(theParamName))
475                                                .anyMatch(t -> systemContainsCode(theQueryParam, t));
476                                case NOT_IN:
477                                        return theSearchParams.myTokenParams.stream()
478                                                .filter(t -> t.getParamName().equals(theParamName))
479                                                .noneMatch(t -> systemContainsCode(theQueryParam, t));
480                                case NOT:
481                                        return !theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam);
482                                default:
483                                        return theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam);
484                        }
485                } else {
486                        return theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam);
487                }
488        }
489
490        private boolean systemContainsCode(TokenParam theQueryParam, ResourceIndexedSearchParamToken theSearchParamToken) {
491                IValidationSupport validationSupport = getValidationSupportOrNull();
492                if (validationSupport == null) {
493                        ourLog.error(Msg.code(2096) + "Attempting to evaluate an unsupported qualifier. This should not happen.");
494                        return false;
495                }
496
497                IValidationSupport.CodeValidationResult codeValidationResult = validationSupport.validateCode(new ValidationSupportContext(validationSupport), new ConceptValidationOptions(), theSearchParamToken.getSystem(), theSearchParamToken.getValue(), null, theQueryParam.getValue());
498                if (codeValidationResult != null) {
499                        return codeValidationResult.isOk();
500                } else {
501                        return false;
502                }
503        }
504
505        private boolean hasChain(IQueryParameterType theParam) {
506                return theParam instanceof ReferenceParam && ((ReferenceParam) theParam).getChain() != null;
507        }
508
509        private boolean hasQualifiers(IQueryParameterType theParam) {
510                return theParam.getQueryParameterQualifier() != null;
511        }
512
513        private InMemoryMatchResult checkUnsupportedPrefixes(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
514                if (theParamDef != null && theParam instanceof BaseParamWithPrefix) {
515                        ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) theParam).getPrefix();
516                        RestSearchParameterTypeEnum paramType = theParamDef.getParamType();
517                        if (!supportedPrefix(prefix, paramType)) {
518                                return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, String.format("The prefix %s is not supported for param type %s", prefix, paramType));
519                        }
520                }
521                return InMemoryMatchResult.successfulMatch();
522        }
523
524        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
525        private boolean supportedPrefix(ParamPrefixEnum theParam, RestSearchParameterTypeEnum theParamType) {
526                if (theParam == null || theParamType == null) {
527                        return true;
528                }
529                switch (theParamType) {
530                        case DATE:
531                                switch (theParam) {
532                                        case GREATERTHAN:
533                                        case GREATERTHAN_OR_EQUALS:
534                                        case LESSTHAN:
535                                        case LESSTHAN_OR_EQUALS:
536                                        case EQUAL:
537                                                return true;
538                                }
539                                break;
540                        default:
541                                return false;
542                }
543                return false;
544        }
545
546        private InMemoryMatchResult checkUnsupportedQualifiers(String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
547                if (hasQualifiers(theParam) && !supportedQualifier(theParamDef, theParam)) {
548                        return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName + theParam.getQueryParameterQualifier(), InMemoryMatchResult.QUALIFIER);
549                }
550                return InMemoryMatchResult.successfulMatch();
551        }
552
553        private boolean supportedQualifier(RuntimeSearchParam theParamDef, IQueryParameterType theParam) {
554                if (theParamDef == null || theParam == null) {
555                        return true;
556                }
557                switch (theParamDef.getParamType()) {
558                        case TOKEN:
559                                TokenParam tokenParam = (TokenParam) theParam;
560                                switch (tokenParam.getModifier()) {
561                                        case IN:
562                                        case NOT_IN:
563                                                // Support for these qualifiers is dependent on an implementation of IValidationSupport being available to delegate the check to
564                                                return getValidationSupportOrNull() != null;
565                                        case NOT:
566                                                return true;
567                                        default:
568                                                return false;
569                                }
570                        default:
571                                return false;
572                }
573        }
574
575        private enum ValidationSupportInitializationState {NOT_INITIALIZED, INITIALIZED, FAILED}
576
577}