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