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