
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 actully 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 return theValue.equals(theId.getValue()) || theValue.equals(theId.getIdPart()); 467 } 468 469 private InMemoryMatchResult matchResourceParam( 470 StorageSettings theStorageSettings, 471 String theParamName, 472 List<List<IQueryParameterType>> theAndOrParams, 473 ResourceIndexedSearchParams theSearchParams, 474 String theResourceName, 475 RuntimeSearchParam theParamDef) { 476 if (theParamDef != null) { 477 switch (theParamDef.getParamType()) { 478 case QUANTITY: 479 case TOKEN: 480 case STRING: 481 case NUMBER: 482 case URI: 483 case DATE: 484 case REFERENCE: 485 if (theSearchParams == null) { 486 return InMemoryMatchResult.successfulMatch(); 487 } else { 488 return InMemoryMatchResult.fromBoolean(theAndOrParams.stream() 489 .allMatch(nextAnd -> matchParams( 490 theStorageSettings, 491 theResourceName, 492 theParamName, 493 theParamDef, 494 nextAnd, 495 theSearchParams))); 496 } 497 case COMPOSITE: 498 case HAS: 499 case SPECIAL: 500 default: 501 return InMemoryMatchResult.unsupportedFromParameterAndReason( 502 theParamName, InMemoryMatchResult.PARAM); 503 } 504 } else { 505 if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { 506 return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM); 507 } else { 508 throw new InvalidRequestException(Msg.code(509) + "Unknown search parameter " + theParamName 509 + " for resource type " + theResourceName); 510 } 511 } 512 } 513 514 private boolean matchParams( 515 StorageSettings theStorageSettings, 516 String theResourceName, 517 String theParamName, 518 RuntimeSearchParam theParamDef, 519 List<? extends IQueryParameterType> theOrList, 520 ResourceIndexedSearchParams theSearchParams) { 521 522 boolean isNegativeTest = isNegative(theParamDef, theOrList); 523 // negative tests like :not and :not-in must not match any or-clause, so we invert the quantifier. 524 if (isNegativeTest) { 525 return theOrList.stream() 526 .allMatch(token -> matchParam( 527 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token)); 528 } else { 529 return theOrList.stream() 530 .anyMatch(token -> matchParam( 531 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token)); 532 } 533 } 534 535 /** 536 * Some modifiers are negative, and must match NONE of their or-list 537 */ 538 private boolean isNegative(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList) { 539 if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) { 540 TokenParam tokenParam = (TokenParam) theOrList.get(0); 541 TokenParamModifier modifier = tokenParam.getModifier(); 542 return modifier != null && modifier.isNegative(); 543 } else { 544 return false; 545 } 546 } 547 548 private boolean matchParam( 549 StorageSettings theStorageSettings, 550 String theResourceName, 551 String theParamName, 552 RuntimeSearchParam theParamDef, 553 ResourceIndexedSearchParams theSearchParams, 554 IQueryParameterType theToken) { 555 if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) { 556 return matchTokenParam( 557 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, (TokenParam) 558 theToken); 559 } else { 560 return theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theToken); 561 } 562 } 563 564 /** 565 * Checks whether a query parameter of type token matches one of the search parameters of an in-memory resource. 566 * The :not modifier is supported. 567 * The :in and :not-in qualifiers are supported only if a bean implementing IValidationSupport is available. 568 * Any other qualifier will be ignored and the match will be treated as unqualified. 569 * 570 * @param theStorageSettings a model configuration 571 * @param theResourceName the name of the resource type being matched 572 * @param theParamName the name of the parameter 573 * @param theParamDef the definition of the search parameter 574 * @param theSearchParams the search parameters derived from the target resource 575 * @param theQueryParam the query parameter to compare with theSearchParams 576 * @return true if theQueryParam matches the collection of theSearchParams, otherwise false 577 */ 578 private boolean matchTokenParam( 579 StorageSettings theStorageSettings, 580 String theResourceName, 581 String theParamName, 582 RuntimeSearchParam theParamDef, 583 ResourceIndexedSearchParams theSearchParams, 584 TokenParam theQueryParam) { 585 if (theQueryParam.getModifier() != null) { 586 switch (theQueryParam.getModifier()) { 587 case IN: 588 return theSearchParams.myTokenParams.stream() 589 .filter(t -> isMatchSearchParam(theStorageSettings, theResourceName, theParamName, t)) 590 .anyMatch(t -> systemContainsCode(theQueryParam, t)); 591 case NOT_IN: 592 return theSearchParams.myTokenParams.stream() 593 .filter(t -> isMatchSearchParam(theStorageSettings, theResourceName, theParamName, t)) 594 .noneMatch(t -> systemContainsCode(theQueryParam, t)); 595 case NOT: 596 return !theSearchParams.matchParam( 597 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 598 case ABOVE: 599 case BELOW: 600 case TEXT: 601 case OF_TYPE: 602 default: 603 return theSearchParams.matchParam( 604 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 605 } 606 } else { 607 return theSearchParams.matchParam( 608 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 609 } 610 } 611 612 private boolean systemContainsCode(TokenParam theQueryParam, ResourceIndexedSearchParamToken theSearchParamToken) { 613 IValidationSupport validationSupport = getValidationSupportOrNull(); 614 if (validationSupport == null) { 615 ourLog.error(Msg.code(2096) + "Attempting to evaluate an unsupported qualifier. This should not happen."); 616 return false; 617 } 618 619 IValidationSupport.CodeValidationResult codeValidationResult = validationSupport.validateCode( 620 new ValidationSupportContext(validationSupport), 621 new ConceptValidationOptions(), 622 theSearchParamToken.getSystem(), 623 theSearchParamToken.getValue(), 624 null, 625 theQueryParam.getValue()); 626 if (codeValidationResult != null) { 627 return codeValidationResult.isOk(); 628 } else { 629 return false; 630 } 631 } 632 633 private boolean hasChain(IQueryParameterType theParam) { 634 return theParam instanceof ReferenceParam && ((ReferenceParam) theParam).getChain() != null; 635 } 636 637 private boolean hasQualifiers(IQueryParameterType theParam) { 638 return theParam.getQueryParameterQualifier() != null; 639 } 640 641 private InMemoryMatchResult checkUnsupportedPrefixes( 642 String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 643 if (theParamDef != null && theParam instanceof BaseParamWithPrefix) { 644 ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) theParam).getPrefix(); 645 RestSearchParameterTypeEnum paramType = theParamDef.getParamType(); 646 if (!supportedPrefix(prefix, paramType)) { 647 return InMemoryMatchResult.unsupportedFromParameterAndReason( 648 theParamName, 649 String.format("The prefix %s is not supported for param type %s", prefix, paramType)); 650 } 651 } 652 return InMemoryMatchResult.successfulMatch(); 653 } 654 655 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 656 private boolean supportedPrefix(ParamPrefixEnum theParam, RestSearchParameterTypeEnum theParamType) { 657 if (theParam == null || theParamType == null) { 658 return true; 659 } 660 switch (theParamType) { 661 case DATE: 662 switch (theParam) { 663 case GREATERTHAN: 664 case GREATERTHAN_OR_EQUALS: 665 case LESSTHAN: 666 case LESSTHAN_OR_EQUALS: 667 case EQUAL: 668 return true; 669 } 670 break; 671 default: 672 return false; 673 } 674 return false; 675 } 676 677 private InMemoryMatchResult checkUnsupportedQualifiers( 678 String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 679 if (hasQualifiers(theParam) && !supportedQualifier(theParamDef, theParam)) { 680 return InMemoryMatchResult.unsupportedFromParameterAndReason( 681 theParamName + theParam.getQueryParameterQualifier(), InMemoryMatchResult.QUALIFIER); 682 } 683 return InMemoryMatchResult.successfulMatch(); 684 } 685 686 private boolean supportedQualifier(RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 687 if (theParamDef == null || theParam == null) { 688 return true; 689 } 690 switch (theParamDef.getParamType()) { 691 case TOKEN: 692 TokenParam tokenParam = (TokenParam) theParam; 693 switch (tokenParam.getModifier()) { 694 case IN: 695 case NOT_IN: 696 // Support for these qualifiers is dependent on an implementation of IValidationSupport being 697 // available to delegate the check to 698 return getValidationSupportOrNull() != null; 699 case NOT: 700 return true; 701 case TEXT: 702 case OF_TYPE: 703 case ABOVE: 704 case BELOW: 705 default: 706 return false; 707 } 708 case NUMBER: 709 case DATE: 710 case STRING: 711 case REFERENCE: 712 case COMPOSITE: 713 case QUANTITY: 714 case URI: 715 case HAS: 716 case SPECIAL: 717 default: 718 return false; 719 } 720 } 721 722 private enum ValidationSupportInitializationState { 723 NOT_INITIALIZED, 724 INITIALIZED, 725 FAILED 726 } 727}