001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.searchparam.matcher; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.context.RuntimeSearchParam; 026import ca.uhn.fhir.context.support.ConceptValidationOptions; 027import ca.uhn.fhir.context.support.IValidationSupport; 028import ca.uhn.fhir.context.support.ValidationSupportContext; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 031import ca.uhn.fhir.jpa.model.entity.StorageSettings; 032import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 033import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 034import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 035import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 036import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; 037import ca.uhn.fhir.jpa.searchparam.util.SourceParam; 038import ca.uhn.fhir.model.api.IQueryParameterType; 039import ca.uhn.fhir.rest.api.Constants; 040import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 041import ca.uhn.fhir.rest.api.server.RequestDetails; 042import ca.uhn.fhir.rest.param.BaseParamWithPrefix; 043import ca.uhn.fhir.rest.param.ParamPrefixEnum; 044import ca.uhn.fhir.rest.param.ReferenceParam; 045import ca.uhn.fhir.rest.param.StringParam; 046import ca.uhn.fhir.rest.param.TokenParam; 047import ca.uhn.fhir.rest.param.TokenParamModifier; 048import ca.uhn.fhir.rest.param.UriParam; 049import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 050import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 051import ca.uhn.fhir.util.MetaUtil; 052import ca.uhn.fhir.util.UrlUtil; 053import com.google.common.collect.Sets; 054import jakarta.annotation.Nonnull; 055import jakarta.annotation.Nullable; 056import org.apache.commons.lang3.StringUtils; 057import org.apache.commons.lang3.Validate; 058import org.hl7.fhir.dstu3.model.Location; 059import org.hl7.fhir.instance.model.api.IAnyResource; 060import org.hl7.fhir.instance.model.api.IBaseCoding; 061import org.hl7.fhir.instance.model.api.IBaseResource; 062import org.hl7.fhir.instance.model.api.IIdType; 063import org.hl7.fhir.instance.model.api.IPrimitiveType; 064import org.slf4j.LoggerFactory; 065import org.springframework.beans.BeansException; 066import org.springframework.beans.factory.annotation.Autowired; 067import org.springframework.context.ApplicationContext; 068 069import java.util.List; 070import java.util.Map; 071import java.util.Set; 072import java.util.stream.Collectors; 073 074import static 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(resourceName, theParamName); 254 InMemoryMatchResult checkUnsupportedResult = 255 checkForUnsupportedParameters(theParamName, paramDef, theAndOrParams); 256 if (!checkUnsupportedResult.supported()) { 257 return checkUnsupportedResult; 258 } 259 260 switch (theParamName) { 261 case IAnyResource.SP_RES_ID: 262 return InMemoryMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource)); 263 case Constants.PARAM_SOURCE: 264 return InMemoryMatchResult.fromBoolean(matchSourcesAndOr(theAndOrParams, theResource)); 265 case Constants.PARAM_TAG: 266 return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, true)); 267 case Constants.PARAM_SECURITY: 268 return InMemoryMatchResult.fromBoolean(matchTagsOrSecurityAndOr(theAndOrParams, theResource, false)); 269 case Constants.PARAM_PROFILE: 270 return InMemoryMatchResult.fromBoolean(matchProfilesAndOr(theAndOrParams, theResource)); 271 default: 272 return matchResourceParam( 273 myStorageSettings, theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); 274 } 275 } 276 277 private InMemoryMatchResult checkForUnsupportedParameters( 278 String theParamName, RuntimeSearchParam theParamDef, List<List<IQueryParameterType>> theAndOrParams) { 279 280 if (UNSUPPORTED_PARAMETER_NAMES.contains(theParamName)) { 281 return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM); 282 } 283 284 for (List<IQueryParameterType> orParams : theAndOrParams) { 285 // The list should never be empty, but better safe than sorry 286 if (orParams.size() > 0) { 287 // The params in each OR list all share the same qualifier, prefix, etc., so we only need to check the 288 // first one 289 InMemoryMatchResult checkUnsupportedResult = 290 checkOneParameterForUnsupportedModifiers(theParamName, theParamDef, orParams.get(0)); 291 if (!checkUnsupportedResult.supported()) { 292 return checkUnsupportedResult; 293 } 294 } 295 } 296 297 return InMemoryMatchResult.successfulMatch(); 298 } 299 300 private InMemoryMatchResult checkOneParameterForUnsupportedModifiers( 301 String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 302 // Assume we're ok until we find evidence we aren't 303 InMemoryMatchResult checkUnsupportedResult = InMemoryMatchResult.successfulMatch(); 304 305 if (hasChain(theParam)) { 306 checkUnsupportedResult = InMemoryMatchResult.unsupportedFromParameterAndReason( 307 theParamName + "." + ((ReferenceParam) theParam).getChain(), InMemoryMatchResult.CHAIN); 308 } 309 310 if (checkUnsupportedResult.supported()) { 311 checkUnsupportedResult = checkUnsupportedQualifiers(theParamName, theParamDef, theParam); 312 } 313 314 if (checkUnsupportedResult.supported()) { 315 checkUnsupportedResult = checkUnsupportedPrefixes(theParamName, theParamDef, theParam); 316 } 317 318 return checkUnsupportedResult; 319 } 320 321 private boolean matchProfilesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) { 322 if (theResource == null) { 323 return true; 324 } 325 return theAndOrParams.stream().allMatch(nextAnd -> matchProfilesOr(nextAnd, theResource)); 326 } 327 328 private boolean matchProfilesOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) { 329 return theOrParams.stream().anyMatch(param -> matchProfile(param, theResource)); 330 } 331 332 private boolean matchProfile(IQueryParameterType theProfileParam, IBaseResource theResource) { 333 UriParam paramProfile = new UriParam(theProfileParam.getValueAsQueryToken(myFhirContext)); 334 335 String paramProfileValue = paramProfile.getValue(); 336 if (isBlank(paramProfileValue)) { 337 return false; 338 } else { 339 return theResource.getMeta().getProfile().stream() 340 .map(IPrimitiveType::getValueAsString) 341 .anyMatch(profileValue -> profileValue != null && profileValue.equals(paramProfileValue)); 342 } 343 } 344 345 private boolean matchSourcesAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) { 346 if (theResource == null) { 347 return true; 348 } 349 return theAndOrParams.stream().allMatch(nextAnd -> matchSourcesOr(nextAnd, theResource)); 350 } 351 352 private boolean matchSourcesOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) { 353 return theOrParams.stream().anyMatch(param -> matchSource(param, theResource)); 354 } 355 356 private boolean matchSource(IQueryParameterType theSourceParam, IBaseResource theResource) { 357 SourceParam paramSource = new SourceParam(theSourceParam.getValueAsQueryToken(myFhirContext)); 358 SourceParam resourceSource = new SourceParam(MetaUtil.getSource(myFhirContext, theResource.getMeta())); 359 boolean matches = true; 360 if (paramSource.getSourceUri() != null) { 361 matches = matchSourceWithModifiers(theSourceParam, paramSource, resourceSource.getSourceUri()); 362 } 363 if (paramSource.getRequestId() != null) { 364 matches &= paramSource.getRequestId().equals(resourceSource.getRequestId()); 365 } 366 return matches; 367 } 368 369 private boolean matchSourceWithModifiers( 370 IQueryParameterType parameterType, SourceParam paramSource, String theSourceUri) { 371 // process :missing modifier 372 if (parameterType.getMissing() != null) { 373 return parameterType.getMissing() == StringUtils.isBlank(theSourceUri); 374 } 375 // process :above, :below, :contains modifiers 376 if (parameterType instanceof UriParam && ((UriParam) parameterType).getQualifier() != null) { 377 UriParam uriParam = ((UriParam) parameterType); 378 switch (uriParam.getQualifier()) { 379 case ABOVE: 380 return UrlUtil.getAboveUriCandidates(paramSource.getSourceUri()).stream() 381 .anyMatch(candidate -> candidate.equals(theSourceUri)); 382 case BELOW: 383 return theSourceUri.startsWith(paramSource.getSourceUri()); 384 case CONTAINS: 385 return StringUtils.containsIgnoreCase(theSourceUri, paramSource.getSourceUri()); 386 default: 387 // Unsupported modifier specified - no match 388 return false; 389 } 390 } else { 391 // no modifiers specified - use equals operator 392 return paramSource.getSourceUri().equals(theSourceUri); 393 } 394 } 395 396 private boolean matchTagsOrSecurityAndOr( 397 List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource, boolean theTag) { 398 if (theResource == null) { 399 return true; 400 } 401 return theAndOrParams.stream().allMatch(nextAnd -> matchTagsOrSecurityOr(nextAnd, theResource, theTag)); 402 } 403 404 private boolean matchTagsOrSecurityOr( 405 List<IQueryParameterType> theOrParams, IBaseResource theResource, boolean theTag) { 406 return theOrParams.stream().anyMatch(param -> matchTagOrSecurity(param, theResource, theTag)); 407 } 408 409 private boolean matchTagOrSecurity(IQueryParameterType theParam, IBaseResource theResource, boolean theTag) { 410 TokenParam param = (TokenParam) theParam; 411 412 List<? extends IBaseCoding> list; 413 if (theTag) { 414 list = theResource.getMeta().getTag(); 415 } else { 416 list = theResource.getMeta().getSecurity(); 417 } 418 boolean haveMatch = false; 419 boolean haveCandidate = false; 420 for (IBaseCoding next : list) { 421 if (param.getSystem() == null && param.getValue() == null) { 422 continue; 423 } 424 haveCandidate = true; 425 if (isNotBlank(param.getSystem())) { 426 if (!param.getSystem().equals(next.getSystem())) { 427 continue; 428 } 429 } 430 if (isNotBlank(param.getValue())) { 431 if (!param.getValue().equals(next.getCode())) { 432 continue; 433 } 434 } 435 haveMatch = true; 436 break; 437 } 438 439 if (param.getModifier() == TokenParamModifier.NOT) { 440 haveMatch = !haveMatch; 441 } 442 443 return haveMatch && haveCandidate; 444 } 445 446 private boolean matchIdsAndOr(List<List<IQueryParameterType>> theAndOrParams, IBaseResource theResource) { 447 if (theResource == null) { 448 return true; 449 } 450 return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource)); 451 } 452 453 private boolean matchIdsOr(List<IQueryParameterType> theOrParams, IBaseResource theResource) { 454 return theOrParams.stream() 455 .anyMatch(param -> param instanceof StringParam 456 && matchId(((StringParam) param).getValue(), theResource.getIdElement())); 457 } 458 459 private boolean matchId(String theValue, IIdType theId) { 460 return theValue.equals(theId.getValue()) || theValue.equals(theId.getIdPart()); 461 } 462 463 private InMemoryMatchResult matchResourceParam( 464 StorageSettings theStorageSettings, 465 String theParamName, 466 List<List<IQueryParameterType>> theAndOrParams, 467 ResourceIndexedSearchParams theSearchParams, 468 String theResourceName, 469 RuntimeSearchParam theParamDef) { 470 if (theParamDef != null) { 471 switch (theParamDef.getParamType()) { 472 case QUANTITY: 473 case TOKEN: 474 case STRING: 475 case NUMBER: 476 case URI: 477 case DATE: 478 case REFERENCE: 479 if (theSearchParams == null) { 480 return InMemoryMatchResult.successfulMatch(); 481 } else { 482 return InMemoryMatchResult.fromBoolean(theAndOrParams.stream() 483 .allMatch(nextAnd -> matchParams( 484 theStorageSettings, 485 theResourceName, 486 theParamName, 487 theParamDef, 488 nextAnd, 489 theSearchParams))); 490 } 491 case COMPOSITE: 492 case HAS: 493 case SPECIAL: 494 default: 495 return InMemoryMatchResult.unsupportedFromParameterAndReason( 496 theParamName, InMemoryMatchResult.PARAM); 497 } 498 } else { 499 if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { 500 return InMemoryMatchResult.unsupportedFromParameterAndReason(theParamName, InMemoryMatchResult.PARAM); 501 } else { 502 throw new InvalidRequestException(Msg.code(509) + "Unknown search parameter " + theParamName 503 + " for resource type " + theResourceName); 504 } 505 } 506 } 507 508 private boolean matchParams( 509 StorageSettings theStorageSettings, 510 String theResourceName, 511 String theParamName, 512 RuntimeSearchParam theParamDef, 513 List<? extends IQueryParameterType> theOrList, 514 ResourceIndexedSearchParams theSearchParams) { 515 516 boolean isNegativeTest = isNegative(theParamDef, theOrList); 517 // negative tests like :not and :not-in must not match any or-clause, so we invert the quantifier. 518 if (isNegativeTest) { 519 return theOrList.stream() 520 .allMatch(token -> matchParam( 521 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token)); 522 } else { 523 return theOrList.stream() 524 .anyMatch(token -> matchParam( 525 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, token)); 526 } 527 } 528 529 /** 530 * Some modifiers are negative, and must match NONE of their or-list 531 */ 532 private boolean isNegative(RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theOrList) { 533 if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) { 534 TokenParam tokenParam = (TokenParam) theOrList.get(0); 535 TokenParamModifier modifier = tokenParam.getModifier(); 536 return modifier != null && modifier.isNegative(); 537 } else { 538 return false; 539 } 540 } 541 542 private boolean matchParam( 543 StorageSettings theStorageSettings, 544 String theResourceName, 545 String theParamName, 546 RuntimeSearchParam theParamDef, 547 ResourceIndexedSearchParams theSearchParams, 548 IQueryParameterType theToken) { 549 if (theParamDef.getParamType().equals(RestSearchParameterTypeEnum.TOKEN)) { 550 return matchTokenParam( 551 theStorageSettings, theResourceName, theParamName, theParamDef, theSearchParams, (TokenParam) 552 theToken); 553 } else { 554 return theSearchParams.matchParam(theStorageSettings, theResourceName, theParamName, theParamDef, theToken); 555 } 556 } 557 558 /** 559 * Checks whether a query parameter of type token matches one of the search parameters of an in-memory resource. 560 * The :not modifier is supported. 561 * The :in and :not-in qualifiers are supported only if a bean implementing IValidationSupport is available. 562 * Any other qualifier will be ignored and the match will be treated as unqualified. 563 * 564 * @param theStorageSettings a model configuration 565 * @param theResourceName the name of the resource type being matched 566 * @param theParamName the name of the parameter 567 * @param theParamDef the definition of the search parameter 568 * @param theSearchParams the search parameters derived from the target resource 569 * @param theQueryParam the query parameter to compare with theSearchParams 570 * @return true if theQueryParam matches the collection of theSearchParams, otherwise false 571 */ 572 private boolean matchTokenParam( 573 StorageSettings theStorageSettings, 574 String theResourceName, 575 String theParamName, 576 RuntimeSearchParam theParamDef, 577 ResourceIndexedSearchParams theSearchParams, 578 TokenParam theQueryParam) { 579 if (theQueryParam.getModifier() != null) { 580 switch (theQueryParam.getModifier()) { 581 case IN: 582 return theSearchParams.myTokenParams.stream() 583 .filter(t -> isMatchSearchParam(theStorageSettings, theResourceName, theParamName, t)) 584 .anyMatch(t -> systemContainsCode(theQueryParam, t)); 585 case NOT_IN: 586 return theSearchParams.myTokenParams.stream() 587 .filter(t -> isMatchSearchParam(theStorageSettings, theResourceName, theParamName, t)) 588 .noneMatch(t -> systemContainsCode(theQueryParam, t)); 589 case NOT: 590 return !theSearchParams.matchParam( 591 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 592 default: 593 return theSearchParams.matchParam( 594 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 595 } 596 } else { 597 return theSearchParams.matchParam( 598 theStorageSettings, theResourceName, theParamName, theParamDef, theQueryParam); 599 } 600 } 601 602 private boolean systemContainsCode(TokenParam theQueryParam, ResourceIndexedSearchParamToken theSearchParamToken) { 603 IValidationSupport validationSupport = getValidationSupportOrNull(); 604 if (validationSupport == null) { 605 ourLog.error(Msg.code(2096) + "Attempting to evaluate an unsupported qualifier. This should not happen."); 606 return false; 607 } 608 609 IValidationSupport.CodeValidationResult codeValidationResult = validationSupport.validateCode( 610 new ValidationSupportContext(validationSupport), 611 new ConceptValidationOptions(), 612 theSearchParamToken.getSystem(), 613 theSearchParamToken.getValue(), 614 null, 615 theQueryParam.getValue()); 616 if (codeValidationResult != null) { 617 return codeValidationResult.isOk(); 618 } else { 619 return false; 620 } 621 } 622 623 private boolean hasChain(IQueryParameterType theParam) { 624 return theParam instanceof ReferenceParam && ((ReferenceParam) theParam).getChain() != null; 625 } 626 627 private boolean hasQualifiers(IQueryParameterType theParam) { 628 return theParam.getQueryParameterQualifier() != null; 629 } 630 631 private InMemoryMatchResult checkUnsupportedPrefixes( 632 String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 633 if (theParamDef != null && theParam instanceof BaseParamWithPrefix) { 634 ParamPrefixEnum prefix = ((BaseParamWithPrefix<?>) theParam).getPrefix(); 635 RestSearchParameterTypeEnum paramType = theParamDef.getParamType(); 636 if (!supportedPrefix(prefix, paramType)) { 637 return InMemoryMatchResult.unsupportedFromParameterAndReason( 638 theParamName, 639 String.format("The prefix %s is not supported for param type %s", prefix, paramType)); 640 } 641 } 642 return InMemoryMatchResult.successfulMatch(); 643 } 644 645 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 646 private boolean supportedPrefix(ParamPrefixEnum theParam, RestSearchParameterTypeEnum theParamType) { 647 if (theParam == null || theParamType == null) { 648 return true; 649 } 650 switch (theParamType) { 651 case DATE: 652 switch (theParam) { 653 case GREATERTHAN: 654 case GREATERTHAN_OR_EQUALS: 655 case LESSTHAN: 656 case LESSTHAN_OR_EQUALS: 657 case EQUAL: 658 return true; 659 } 660 break; 661 default: 662 return false; 663 } 664 return false; 665 } 666 667 private InMemoryMatchResult checkUnsupportedQualifiers( 668 String theParamName, RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 669 if (hasQualifiers(theParam) && !supportedQualifier(theParamDef, theParam)) { 670 return InMemoryMatchResult.unsupportedFromParameterAndReason( 671 theParamName + theParam.getQueryParameterQualifier(), InMemoryMatchResult.QUALIFIER); 672 } 673 return InMemoryMatchResult.successfulMatch(); 674 } 675 676 private boolean supportedQualifier(RuntimeSearchParam theParamDef, IQueryParameterType theParam) { 677 if (theParamDef == null || theParam == null) { 678 return true; 679 } 680 switch (theParamDef.getParamType()) { 681 case TOKEN: 682 TokenParam tokenParam = (TokenParam) theParam; 683 switch (tokenParam.getModifier()) { 684 case IN: 685 case NOT_IN: 686 // Support for these qualifiers is dependent on an implementation of IValidationSupport being 687 // available to delegate the check to 688 return getValidationSupportOrNull() != null; 689 case NOT: 690 return true; 691 default: 692 return false; 693 } 694 default: 695 return false; 696 } 697 } 698 699 private enum ValidationSupportInitializationState { 700 NOT_INITIALIZED, 701 INITIALIZED, 702 FAILED 703 } 704}