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}