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