
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.extractor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.interceptor.model.RequestPartitionId; 030import ca.uhn.fhir.jpa.model.config.PartitionSettings; 031import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 032import ca.uhn.fhir.jpa.model.dao.JpaPid; 033import ca.uhn.fhir.jpa.model.entity.BasePartitionable; 034import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 035import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter; 036import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 037import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 038import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; 039import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; 040import ca.uhn.fhir.jpa.model.entity.ResourceLink; 041import ca.uhn.fhir.jpa.model.entity.ResourceLink.ResourceLinkForLocalReferenceParams; 042import ca.uhn.fhir.jpa.model.entity.ResourceTable; 043import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity; 044import ca.uhn.fhir.jpa.model.entity.StorageSettings; 045import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 046import ca.uhn.fhir.jpa.searchparam.util.RuntimeSearchParamHelper; 047import ca.uhn.fhir.parser.DataFormatException; 048import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 049import ca.uhn.fhir.rest.api.server.RequestDetails; 050import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 051import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 052import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 053import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 054import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 055import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 056import ca.uhn.fhir.util.FhirTerser; 057import com.google.common.annotations.VisibleForTesting; 058import jakarta.annotation.Nonnull; 059import jakarta.annotation.Nullable; 060import org.apache.commons.lang3.StringUtils; 061import org.hl7.fhir.instance.model.api.IBaseReference; 062import org.hl7.fhir.instance.model.api.IBaseResource; 063import org.hl7.fhir.instance.model.api.IIdType; 064import org.hl7.fhir.r4.model.IdType; 065import org.springframework.beans.factory.annotation.Autowired; 066 067import java.util.ArrayList; 068import java.util.Collection; 069import java.util.Date; 070import java.util.HashMap; 071import java.util.HashSet; 072import java.util.List; 073import java.util.Map; 074import java.util.Optional; 075import java.util.Set; 076import java.util.stream.Collectors; 077 078import static ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED; 079import static ca.uhn.fhir.jpa.model.entity.ResourceLink.forLocalReference; 080import static org.apache.commons.lang3.StringUtils.isBlank; 081import static org.apache.commons.lang3.StringUtils.isNotBlank; 082 083public class SearchParamExtractorService { 084 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class); 085 086 @Autowired 087 private ISearchParamExtractor mySearchParamExtractor; 088 089 @Autowired 090 private IInterceptorBroadcaster myInterceptorBroadcaster; 091 092 @Autowired 093 private StorageSettings myStorageSettings; 094 095 @Autowired 096 private FhirContext myContext; 097 098 @Autowired 099 private ISearchParamRegistry mySearchParamRegistry; 100 101 @Autowired 102 private PartitionSettings myPartitionSettings; 103 104 @Autowired(required = false) 105 private IResourceLinkResolver myResourceLinkResolver; 106 107 private SearchParamExtractionUtil mySearchParamExtractionUtil; 108 109 @VisibleForTesting 110 public void setSearchParamExtractor(ISearchParamExtractor theSearchParamExtractor) { 111 mySearchParamExtractor = theSearchParamExtractor; 112 } 113 114 /** 115 * This method is responsible for scanning a resource for all the search parameter instances. 116 * I.e. for all search parameters defined for 117 * a given resource type, it extracts the associated indexes and populates 118 * {@literal theParams}. 119 */ 120 public void extractFromResource( 121 RequestPartitionId theRequestPartitionId, 122 RequestDetails theRequestDetails, 123 ResourceIndexedSearchParams theNewParams, 124 ResourceIndexedSearchParams theExistingParams, 125 ResourceTable theEntity, 126 IBaseResource theResource, 127 TransactionDetails theTransactionDetails, 128 boolean theFailOnInvalidReference, 129 @Nonnull ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) { 130 131 /* 132 * The FHIRPath evaluator doesn't know how to handle reference which are 133 * stored by value in Reference#setResource(IBaseResource) as opposed to being 134 * stored by reference in Reference#setReference(String). It also doesn't know 135 * how to handle contained resources where the ID contains a hash mark (which 136 * was the default in HAPI FHIR until 8.2.0 but became disallowed by the 137 * FHIRPath evaluator in that version. So prior to indexing we will now always 138 * clean references up. 139 */ 140 myContext.newTerser().containResources(theResource, null, true); 141 142 // All search parameter types except Reference 143 ResourceIndexedSearchParams normalParams = ResourceIndexedSearchParams.withSets(); 144 getExtractionUtil() 145 .extractSearchIndexParameters(theRequestDetails, normalParams, theResource, theSearchParamFilter); 146 mergeParams(normalParams, theNewParams); 147 148 // Reference search parameters 149 boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources(); 150 ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = 151 mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources); 152 SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences); 153 154 if (indexOnContainedResources) { 155 ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets(); 156 extractSearchIndexParametersForContainedResources( 157 theRequestDetails, containedParams, theResource, theEntity, indexedReferences); 158 mergeParams(containedParams, theNewParams); 159 } 160 161 if (myStorageSettings.isIndexOnUpliftedRefchains()) { 162 ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets(); 163 extractSearchIndexParametersForUpliftedRefchains( 164 theRequestDetails, 165 containedParams, 166 theEntity, 167 theRequestPartitionId, 168 theTransactionDetails, 169 indexedReferences); 170 mergeParams(containedParams, theNewParams); 171 } 172 173 // Do this after, because we add to strings during both string and token processing, and contained resource if 174 // any 175 populateResourceTables(theNewParams, theEntity); 176 177 // Reference search parameters 178 extractResourceLinks( 179 theRequestPartitionId, 180 theExistingParams, 181 theNewParams, 182 theEntity, 183 theResource, 184 theTransactionDetails, 185 theFailOnInvalidReference, 186 theRequestDetails, 187 indexedReferences); 188 189 if (indexOnContainedResources) { 190 extractResourceLinksForContainedResources( 191 theRequestPartitionId, 192 theNewParams, 193 theEntity, 194 theResource, 195 theTransactionDetails, 196 theFailOnInvalidReference, 197 theRequestDetails); 198 } 199 200 // Missing (:missing) Indexes - These are indexes to satisfy the :missing 201 // modifier 202 if (myStorageSettings.getIndexMissingFields() == StorageSettings.IndexEnabledEnum.ENABLED) { 203 204 // References 205 Map<String, Boolean> presenceMap = getReferenceSearchParamPresenceMap(theEntity, theNewParams); 206 presenceMap.forEach((key, value) -> { 207 SearchParamPresentEntity present = new SearchParamPresentEntity(); 208 present.setPartitionSettings(myPartitionSettings); 209 present.setResource(theEntity); 210 present.setParamName(key); 211 present.setPresent(value); 212 present.setPartitionId(theEntity.getPartitionId()); 213 present.calculateHashes(); 214 theNewParams.mySearchParamPresentEntities.add(present); 215 }); 216 217 // Everything else 218 ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( 219 theEntity.getResourceType(), ISearchParamRegistry.SearchParamLookupContextEnum.INDEX); 220 theNewParams.findMissingSearchParams(myPartitionSettings, myStorageSettings, theEntity, activeSearchParams); 221 } 222 223 extractSearchParamComboUnique(theEntity, theNewParams); 224 225 extractSearchParamComboNonUnique(theEntity, theNewParams); 226 227 theNewParams.setUpdatedTime(theTransactionDetails.getTransactionDate()); 228 } 229 230 private SearchParamExtractionUtil getExtractionUtil() { 231 if (mySearchParamExtractionUtil == null) { 232 mySearchParamExtractionUtil = new SearchParamExtractionUtil( 233 myContext, myStorageSettings, mySearchParamExtractor, myInterceptorBroadcaster); 234 } 235 return mySearchParamExtractionUtil; 236 } 237 238 @Nonnull 239 private Map<String, Boolean> getReferenceSearchParamPresenceMap( 240 ResourceTable entity, ResourceIndexedSearchParams newParams) { 241 Map<String, Boolean> retval = new HashMap<>(); 242 243 for (String nextKey : newParams.getPopulatedResourceLinkParameters()) { 244 retval.put(nextKey, Boolean.TRUE); 245 } 246 247 ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( 248 entity.getResourceType(), ISearchParamRegistry.SearchParamLookupContextEnum.INDEX); 249 for (RuntimeSearchParam nextParam : activeSearchParams.values()) { 250 if (nextParam.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { 251 continue; 252 } 253 if (RuntimeSearchParamHelper.isSpeciallyHandledSearchParameter(nextParam, myStorageSettings)) { 254 continue; 255 } 256 257 retval.putIfAbsent(nextParam.getName(), Boolean.FALSE); 258 } 259 260 return retval; 261 } 262 263 @VisibleForTesting 264 public void setStorageSettings(StorageSettings theStorageSettings) { 265 myStorageSettings = theStorageSettings; 266 } 267 268 /** 269 * Extract search parameter indexes for contained resources. E.g. if we 270 * are storing a Patient with a contained Organization, we might extract 271 * a String index on the Patient with paramName="organization.name" and 272 * value="Org Name" 273 */ 274 private void extractSearchIndexParametersForContainedResources( 275 RequestDetails theRequestDetails, 276 ResourceIndexedSearchParams theParams, 277 IBaseResource theResource, 278 ResourceTable theEntity, 279 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 280 281 FhirTerser terser = myContext.newTerser(); 282 283 // 1. get all contained resources 284 Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false); 285 286 // Extract search parameters 287 IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() { 288 @Nonnull 289 @Override 290 public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) { 291 // Currently for contained resources we always index all search parameters 292 // on all contained resources. A potential nice future optimization would 293 // be to make this configurable, perhaps with an optional extension you could 294 // add to a SearchParameter? 295 return ISearchParamExtractor.ALL_PARAMS; 296 } 297 298 @Override 299 public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) { 300 if (thePathAndRef.getRef() == null) { 301 return null; 302 } 303 return findContainedResource(containedResources, thePathAndRef.getRef()); 304 } 305 }; 306 boolean recurse = myStorageSettings.isIndexOnContainedResourcesRecursively(); 307 extractSearchIndexParametersForTargetResources( 308 theRequestDetails, 309 theParams, 310 theEntity, 311 new HashSet<>(), 312 strategy, 313 theIndexedReferences, 314 recurse, 315 true); 316 } 317 318 /** 319 * Extract search parameter indexes for uplifted refchains. E.g. if we 320 * are storing a Patient with reference to an Organization and the 321 * "Patient:organization" SearchParameter declares an uplifted refchain 322 * on the "name" SearchParameter, we might extract a String index 323 * on the Patient with paramName="organization.name" and value="Org Name" 324 */ 325 private void extractSearchIndexParametersForUpliftedRefchains( 326 RequestDetails theRequestDetails, 327 ResourceIndexedSearchParams theParams, 328 ResourceTable theEntity, 329 RequestPartitionId theRequestPartitionId, 330 TransactionDetails theTransactionDetails, 331 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 332 IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() { 333 334 @Nonnull 335 @Override 336 public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) { 337 String searchParamName = thePathAndRef.getSearchParamName(); 338 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam( 339 theEntity.getResourceType(), 340 searchParamName, 341 ISearchParamRegistry.SearchParamLookupContextEnum.INDEX); 342 Set<String> upliftRefchainCodes = searchParam.getUpliftRefchainCodes(); 343 if (upliftRefchainCodes.isEmpty()) { 344 return ISearchParamExtractor.NO_PARAMS; 345 } 346 return sp -> sp.stream() 347 .filter(t -> upliftRefchainCodes.contains(t.getName())) 348 .collect(Collectors.toList()); 349 } 350 351 @Override 352 public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) { 353 // The PathAndRef will contain a resource if the SP path was inside a Bundle 354 // and pointed to a resource (e.g. Bundle.entry.resource) as opposed to 355 // pointing to a reference (e.g. Observation.subject) 356 if (thePathAndRef.getResource() != null) { 357 return thePathAndRef.getResource(); 358 } 359 360 // Ok, it's a normal reference 361 IIdType reference = thePathAndRef.getRef().getReferenceElement(); 362 363 // If we're processing a FHIR transaction, we store the resources 364 // mapped by their resolved resource IDs in theTransactionDetails 365 IBaseResource resolvedResource = theTransactionDetails.getResolvedResource(reference); 366 367 // And the usual case is that the reference points to a resource 368 // elsewhere in the repository, so we load it 369 if (resolvedResource == null 370 && myResourceLinkResolver != null 371 && !reference.getValue().startsWith("urn:uuid:")) { 372 RequestPartitionId targetRequestPartitionId = determineResolverPartitionId(theRequestPartitionId); 373 resolvedResource = myResourceLinkResolver.loadTargetResource( 374 targetRequestPartitionId, 375 theEntity.getResourceType(), 376 thePathAndRef, 377 theRequestDetails, 378 theTransactionDetails); 379 if (resolvedResource != null) { 380 ourLog.trace("Found target: {}", resolvedResource.getIdElement()); 381 theTransactionDetails.addResolvedResource( 382 thePathAndRef.getRef().getReferenceElement(), resolvedResource); 383 } 384 } 385 386 return resolvedResource; 387 } 388 }; 389 extractSearchIndexParametersForTargetResources( 390 theRequestDetails, theParams, theEntity, new HashSet<>(), strategy, theIndexedReferences, false, false); 391 } 392 393 /** 394 * Extract indexes for contained references as well as for uplifted refchains. 395 * These two types of indexes are both similar special cases. Normally we handle 396 * chained searches ("Patient?organization.name=Foo") using a join from the 397 * {@link ResourceLink} table (for the "organization" part) to the 398 * {@link ResourceIndexedSearchParamString} table (for the "name" part). But 399 * for both contained resource indexes and uplifted refchains we use only the 400 * {@link ResourceIndexedSearchParamString} table to handle the entire 401 * "organization.name" part, or the other similar tables for token, number, etc. 402 * 403 * @see #extractSearchIndexParametersForContainedResources(RequestDetails, ResourceIndexedSearchParams, IBaseResource, ResourceTable, ISearchParamExtractor.SearchParamSet) 404 * @see #extractSearchIndexParametersForUpliftedRefchains(RequestDetails, ResourceIndexedSearchParams, ResourceTable, RequestPartitionId, TransactionDetails, ISearchParamExtractor.SearchParamSet) 405 */ 406 private void extractSearchIndexParametersForTargetResources( 407 RequestDetails theRequestDetails, 408 ResourceIndexedSearchParams theParams, 409 ResourceTable theEntity, 410 Collection<IBaseResource> theAlreadySeenResources, 411 IChainedSearchParameterExtractionStrategy theTargetIndexingStrategy, 412 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences, 413 boolean theRecurse, 414 boolean theIndexOnContainedResources) { 415 // 2. Find referenced search parameters 416 417 String spnamePrefix; 418 // 3. for each referenced search parameter, create an index 419 for (PathAndRef nextPathAndRef : theIndexedReferences) { 420 421 // 3.1 get the search parameter name as spname prefix 422 spnamePrefix = nextPathAndRef.getSearchParamName(); 423 424 if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null)) 425 continue; 426 427 // 3.1.2 check if this ref actually applies here 428 ISearchParamExtractor.ISearchParamFilter searchParamsToIndex = 429 theTargetIndexingStrategy.getSearchParamFilter(nextPathAndRef); 430 if (searchParamsToIndex == ISearchParamExtractor.NO_PARAMS) { 431 continue; 432 } 433 434 // 3.2 find the target resource 435 IBaseResource targetResource = theTargetIndexingStrategy.fetchResourceAtPath(nextPathAndRef); 436 if (targetResource == null) continue; 437 438 // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite 439 // loops 440 if (theAlreadySeenResources.contains(targetResource)) { 441 continue; 442 } 443 444 ResourceIndexedSearchParams currParams = ResourceIndexedSearchParams.withSets(); 445 446 // 3.3 create indexes for the current contained resource 447 getExtractionUtil() 448 .extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex); 449 450 // 3.4 recurse to process any other contained resources referenced by this one 451 // Recursing is currently only allowed for contained resources and not 452 // uplifted refchains because the latter could potentially kill performance 453 // with the number of resource resolutions needed in order to handle 454 // a single write. Maybe in the future we could add caching to improve 455 // this 456 if (theRecurse) { 457 HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); 458 nextAlreadySeenResources.add(targetResource); 459 460 ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = 461 mySearchParamExtractor.extractResourceLinks(targetResource, theIndexOnContainedResources); 462 SearchParamExtractorService.handleWarnings( 463 theRequestDetails, myInterceptorBroadcaster, indexedReferences); 464 465 extractSearchIndexParametersForTargetResources( 466 theRequestDetails, 467 currParams, 468 theEntity, 469 nextAlreadySeenResources, 470 theTargetIndexingStrategy, 471 indexedReferences, 472 true, 473 theIndexOnContainedResources); 474 } 475 476 // 3.5 added reference name as a prefix for the contained resource if any 477 // e.g. for Observation.subject contained reference 478 // the SP_NAME = subject.family 479 currParams.updateSpnamePrefixForIndexOnUpliftedChain( 480 theEntity.getResourceType(), nextPathAndRef.getSearchParamName()); 481 482 // 3.6 merge to the mainParams 483 // NOTE: the spname prefix is different 484 mergeParams(currParams, theParams); 485 } 486 } 487 488 private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) { 489 for (IBaseResource resource : resources) { 490 String referenceString = reference.getReferenceElement().getValue(); 491 if (referenceString != null && referenceString.length() > 1) { 492 referenceString = referenceString.substring(1); 493 if (resource.getIdElement().getValue().equals(referenceString)) { 494 return resource; 495 } 496 } 497 } 498 return null; 499 } 500 501 private void mergeParams(ResourceIndexedSearchParams theSrcParams, ResourceIndexedSearchParams theTargetParams) { 502 503 theTargetParams.myNumberParams.addAll(theSrcParams.myNumberParams); 504 theTargetParams.myQuantityParams.addAll(theSrcParams.myQuantityParams); 505 theTargetParams.myQuantityNormalizedParams.addAll(theSrcParams.myQuantityNormalizedParams); 506 theTargetParams.myDateParams.addAll(theSrcParams.myDateParams); 507 theTargetParams.myUriParams.addAll(theSrcParams.myUriParams); 508 theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams); 509 theTargetParams.myStringParams.addAll(theSrcParams.myStringParams); 510 theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams); 511 theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams); 512 } 513 514 private void populateResourceTables(ResourceIndexedSearchParams theParams, ResourceTable theEntity) { 515 516 populateResourceTable(theParams.myNumberParams, theEntity); 517 populateResourceTable(theParams.myQuantityParams, theEntity); 518 populateResourceTable(theParams.myQuantityNormalizedParams, theEntity); 519 populateResourceTable(theParams.myDateParams, theEntity); 520 populateResourceTable(theParams.myUriParams, theEntity); 521 populateResourceTable(theParams.myTokenParams, theEntity); 522 populateResourceTable(theParams.myStringParams, theEntity); 523 populateResourceTable(theParams.myCoordsParams, theEntity); 524 } 525 526 @VisibleForTesting 527 public void setContext(FhirContext theContext) { 528 myContext = theContext; 529 } 530 531 private void extractResourceLinks( 532 RequestPartitionId theRequestPartitionId, 533 ResourceIndexedSearchParams theParams, 534 ResourceTable theEntity, 535 IBaseResource theResource, 536 TransactionDetails theTransactionDetails, 537 boolean theFailOnInvalidReference, 538 RequestDetails theRequest, 539 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 540 extractResourceLinks( 541 theRequestPartitionId, 542 ResourceIndexedSearchParams.withSets(), 543 theParams, 544 theEntity, 545 theResource, 546 theTransactionDetails, 547 theFailOnInvalidReference, 548 theRequest, 549 theIndexedReferences); 550 } 551 552 private void extractResourceLinks( 553 RequestPartitionId theRequestPartitionId, 554 ResourceIndexedSearchParams theExistingParams, 555 ResourceIndexedSearchParams theNewParams, 556 ResourceTable theEntity, 557 IBaseResource theResource, 558 TransactionDetails theTransactionDetails, 559 boolean theFailOnInvalidReference, 560 RequestDetails theRequest, 561 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 562 String sourceResourceName = myContext.getResourceType(theResource); 563 564 for (PathAndRef nextPathAndRef : theIndexedReferences) { 565 if (nextPathAndRef.getRef() != null) { 566 if (nextPathAndRef.getRef().getReferenceElement().isLocal()) { 567 continue; 568 } 569 570 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam( 571 sourceResourceName, 572 nextPathAndRef.getSearchParamName(), 573 ISearchParamRegistry.SearchParamLookupContextEnum.INDEX); 574 extractResourceLinks( 575 theRequestPartitionId, 576 theExistingParams, 577 theNewParams, 578 theEntity, 579 theTransactionDetails, 580 sourceResourceName, 581 searchParam, 582 nextPathAndRef, 583 theFailOnInvalidReference, 584 theRequest); 585 } 586 } 587 588 theEntity.setHasLinks(!theNewParams.myLinks.isEmpty()); 589 } 590 591 private void extractResourceLinks( 592 RequestPartitionId theRequestPartitionId, 593 ResourceIndexedSearchParams theExistingParams, 594 ResourceIndexedSearchParams theNewParams, 595 ResourceTable theEntity, 596 TransactionDetails theTransactionDetails, 597 String theSourceResourceName, 598 RuntimeSearchParam theRuntimeSearchParam, 599 PathAndRef thePathAndRef, 600 boolean theFailOnInvalidReference, 601 RequestDetails theRequest) { 602 IBaseReference nextReference = thePathAndRef.getRef(); 603 IIdType nextId = nextReference.getReferenceElement(); 604 String path = thePathAndRef.getPath(); 605 Date transactionDate = theTransactionDetails.getTransactionDate(); 606 607 /* 608 * This can only really happen if the DAO is being called 609 * programmatically with a Bundle (not through the FHIR REST API) 610 * but Smile does this 611 */ 612 if (nextId.isEmpty() && nextReference.getResource() != null) { 613 nextId = nextReference.getResource().getIdElement(); 614 } 615 616 if (nextId.hasVersionIdPart() && shouldStripVersionFromReferenceAtPath(thePathAndRef.getPath())) { 617 nextId = nextId.toVersionless(); 618 } 619 620 theNewParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName()); 621 622 boolean canonical = thePathAndRef.isCanonical(); 623 if (LogicalReferenceHelper.isLogicalReference(myStorageSettings, nextId) || canonical) { 624 String value = nextId.getValue(); 625 ResourceLink resourceLink = 626 ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, transactionDate); 627 if (theNewParams.myLinks.add(resourceLink)) { 628 ourLog.debug("Indexing remote resource reference URL: {}", nextId); 629 } 630 return; 631 } 632 633 String baseUrl = nextId.getBaseUrl(); 634 635 // If this is a conditional URL, the part after the question mark 636 // can include URLs (e.g. token system URLs) and these really confuse 637 // the IdType parser because a conditional URL isn't actually a valid 638 // FHIR ID. So in order to truly determine whether we're dealing with 639 // an absolute reference, we strip the query part and reparse 640 // the reference. 641 int questionMarkIndex = nextId.getValue().indexOf('?'); 642 if (questionMarkIndex != -1) { 643 IdType preQueryId = new IdType(nextId.getValue().substring(0, questionMarkIndex - 1)); 644 baseUrl = preQueryId.getBaseUrl(); 645 } 646 647 String typeString = nextId.getResourceType(); 648 if (isBlank(typeString)) { 649 String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - " 650 + nextId.getValue(); 651 if (theFailOnInvalidReference) { 652 throw new InvalidRequestException(Msg.code(505) + msg); 653 } else { 654 ourLog.debug(msg); 655 return; 656 } 657 } 658 RuntimeResourceDefinition resourceDefinition; 659 try { 660 resourceDefinition = myContext.getResourceDefinition(typeString); 661 } catch (DataFormatException e) { 662 String msg = "Invalid resource reference found at path[" + path 663 + "] - Resource type is unknown or not supported on this server - " + nextId.getValue(); 664 if (theFailOnInvalidReference) { 665 throw new InvalidRequestException(Msg.code(506) + msg); 666 } else { 667 ourLog.debug(msg); 668 return; 669 } 670 } 671 672 if (theRuntimeSearchParam.hasTargets()) { 673 if (!theRuntimeSearchParam.getTargets().contains(typeString)) { 674 return; 675 } 676 } 677 678 if (isNotBlank(baseUrl)) { 679 if (!myStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl) 680 && !myStorageSettings.isAllowExternalReferences()) { 681 String msg = myContext 682 .getLocalizer() 683 .getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue()); 684 throw new InvalidRequestException(Msg.code(507) + msg); 685 } else { 686 ResourceLink resourceLink = 687 ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, transactionDate); 688 if (theNewParams.myLinks.add(resourceLink)) { 689 ourLog.debug("Indexing remote resource reference URL: {}", nextId); 690 } 691 return; 692 } 693 } 694 695 Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass(); 696 String targetId = nextId.getIdPart(); 697 if (StringUtils.isBlank(targetId)) { 698 String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " 699 + nextId.getValue(); 700 if (theFailOnInvalidReference) { 701 throw new InvalidRequestException(Msg.code(508) + msg); 702 } else { 703 ourLog.debug(msg); 704 return; 705 } 706 } 707 708 IIdType referenceElement = thePathAndRef.getRef().getReferenceElement(); 709 if (isBlank(referenceElement.getValue())) { 710 // it's an embedded element maybe; 711 // we need a valid referenceElement, becuase we 712 // resolve the resource by this value (and if we use "null", we can't resolve multiple values) 713 referenceElement = thePathAndRef.getRef().getResource().getIdElement(); 714 } 715 JpaPid resolvedTargetId = (JpaPid) theTransactionDetails.getResolvedResourceId(referenceElement); 716 ResourceLink resourceLink; 717 718 Long targetVersionId = nextId.getVersionIdPartAsLong(); 719 if (resolvedTargetId != null) { 720 721 /* 722 * If we have already resolved the given reference within this transaction, we don't 723 * need to resolve it again 724 */ 725 myResourceLinkResolver.validateTypeOrThrowException(type); 726 727 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 728 .setSourcePath(thePathAndRef.getPath()) 729 .setSourceResource(theEntity) 730 .setTargetResourceType(typeString) 731 .setTargetResourcePid(resolvedTargetId.getId()) 732 .setTargetResourceId(targetId) 733 .setUpdated(transactionDate) 734 .setTargetResourceVersion(targetVersionId) 735 .setTargetResourcePartitionablePartitionId(resolvedTargetId.getPartitionablePartitionId()); 736 737 resourceLink = forLocalReference(params); 738 739 } else if (theFailOnInvalidReference) { 740 741 /* 742 * The reference points to another resource, so let's look it up. We need to do this 743 * since the target may be a forced ID, but also so that we can throw an exception 744 * if the reference is invalid 745 */ 746 myResourceLinkResolver.validateTypeOrThrowException(type); 747 748 /* 749 * We need to obtain a resourceLink out of the provided {@literal thePathAndRef}. In the case 750 * where we are updating a resource that already has resourceLinks (stored in {@literal theExistingParams.getResourceLinks()}), 751 * let's try to match thePathAndRef to an already existing resourceLink to avoid the 752 * very expensive operation of creating a resourceLink that would end up being exactly the same 753 * one we already have. 754 */ 755 Optional<ResourceLink> optionalResourceLink = 756 findMatchingResourceLink(thePathAndRef, theExistingParams.getResourceLinks()); 757 if (optionalResourceLink.isPresent()) { 758 resourceLink = optionalResourceLink.get(); 759 } else { 760 resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull( 761 theRequestPartitionId, 762 theSourceResourceName, 763 thePathAndRef, 764 theEntity, 765 transactionDate, 766 nextId, 767 theRequest, 768 theTransactionDetails); 769 } 770 771 if (resourceLink == null) { 772 return; 773 } else { 774 // Cache the outcome in the current transaction in case there are more references 775 JpaPid persistentId = 776 JpaPid.fromId(resourceLink.getTargetResourcePid(), resourceLink.getTargetResourcePartitionId()); 777 persistentId.setPartitionablePartitionId(PartitionablePartitionId.with( 778 resourceLink.getTargetResourcePartitionId(), resourceLink.getTargetResourcePartitionDate())); 779 theTransactionDetails.addResolvedResourceId(referenceElement, persistentId); 780 } 781 782 } else { 783 784 /* 785 * Just assume the reference is valid. This is used for in-memory matching since there 786 * is no expectation of a database in this situation 787 */ 788 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 789 .setSourcePath(thePathAndRef.getPath()) 790 .setSourceResource(theEntity) 791 .setTargetResourceType(typeString) 792 .setTargetResourceId(targetId) 793 .setUpdated(transactionDate) 794 .setTargetResourceVersion(targetVersionId); 795 796 resourceLink = forLocalReference(params); 797 } 798 799 theNewParams.myLinks.add(resourceLink); 800 } 801 802 private Optional<ResourceLink> findMatchingResourceLink( 803 PathAndRef thePathAndRef, Collection<ResourceLink> theResourceLinks) { 804 IIdType referenceElement = thePathAndRef.getRef().getReferenceElement(); 805 List<ResourceLink> resourceLinks = new ArrayList<>(theResourceLinks); 806 for (ResourceLink resourceLink : resourceLinks) { 807 808 // comparing the searchParam path ex: Group.member.entity 809 boolean hasMatchingSearchParamPath = 810 StringUtils.equals(resourceLink.getSourcePath(), thePathAndRef.getPath()); 811 812 boolean hasMatchingResourceType = 813 StringUtils.equals(resourceLink.getTargetResourceType(), referenceElement.getResourceType()); 814 815 boolean hasMatchingResourceId = 816 StringUtils.equals(resourceLink.getTargetResourceId(), referenceElement.getIdPart()); 817 818 boolean hasMatchingResourceVersion = myContext.getParserOptions().isStripVersionsFromReferences() 819 || referenceElement.getVersionIdPartAsLong() == null 820 || referenceElement.getVersionIdPartAsLong().equals(resourceLink.getTargetResourceVersion()); 821 822 if (hasMatchingSearchParamPath 823 && hasMatchingResourceType 824 && hasMatchingResourceId 825 && hasMatchingResourceVersion) { 826 return Optional.of(resourceLink); 827 } 828 } 829 830 return Optional.empty(); 831 } 832 833 private void extractResourceLinksForContainedResources( 834 RequestPartitionId theRequestPartitionId, 835 ResourceIndexedSearchParams theParams, 836 ResourceTable theEntity, 837 IBaseResource theResource, 838 TransactionDetails theTransactionDetails, 839 boolean theFailOnInvalidReference, 840 RequestDetails theRequest) { 841 842 FhirTerser terser = myContext.newTerser(); 843 844 // 1. get all contained resources 845 Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false); 846 847 extractResourceLinksForContainedResources( 848 theRequestPartitionId, 849 theParams, 850 theEntity, 851 theResource, 852 theTransactionDetails, 853 theFailOnInvalidReference, 854 theRequest, 855 containedResources, 856 new HashSet<>()); 857 } 858 859 private void extractResourceLinksForContainedResources( 860 RequestPartitionId theRequestPartitionId, 861 ResourceIndexedSearchParams theParams, 862 ResourceTable theEntity, 863 IBaseResource theResource, 864 TransactionDetails theTransactionDetails, 865 boolean theFailOnInvalidReference, 866 RequestDetails theRequest, 867 Collection<IBaseResource> theContainedResources, 868 Collection<IBaseResource> theAlreadySeenResources) { 869 870 // 2. Find referenced search parameters 871 ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = 872 mySearchParamExtractor.extractResourceLinks(theResource, true); 873 874 String spNamePrefix; 875 ResourceIndexedSearchParams currParams; 876 // 3. for each referenced search parameter, create an index 877 for (PathAndRef nextPathAndRef : referencedSearchParamSet) { 878 879 // 3.1 get the search parameter name as spname prefix 880 spNamePrefix = nextPathAndRef.getSearchParamName(); 881 882 if (spNamePrefix == null || nextPathAndRef.getRef() == null) continue; 883 884 // 3.2 find the contained resource 885 IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef()); 886 if (containedResource == null) continue; 887 888 // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite 889 // loops 890 if (theAlreadySeenResources.contains(containedResource)) { 891 continue; 892 } 893 894 currParams = ResourceIndexedSearchParams.withSets(); 895 896 // 3.3 create indexes for the current contained resource 897 ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = 898 mySearchParamExtractor.extractResourceLinks(containedResource, true); 899 extractResourceLinks( 900 theRequestPartitionId, 901 currParams, 902 theEntity, 903 containedResource, 904 theTransactionDetails, 905 theFailOnInvalidReference, 906 theRequest, 907 indexedReferences); 908 909 // 3.4 recurse to process any other contained resources referenced by this one 910 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 911 HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); 912 nextAlreadySeenResources.add(containedResource); 913 extractResourceLinksForContainedResources( 914 theRequestPartitionId, 915 currParams, 916 theEntity, 917 containedResource, 918 theTransactionDetails, 919 theFailOnInvalidReference, 920 theRequest, 921 theContainedResources, 922 nextAlreadySeenResources); 923 } 924 925 // 3.4 added reference name as a prefix for the contained resource if any 926 // e.g. for Observation.subject contained reference 927 // the SP_NAME = subject.family 928 currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath()); 929 930 // 3.5 merge to the mainParams 931 // NOTE: the spname prefix is different 932 theParams.getResourceLinks().addAll(currParams.getResourceLinks()); 933 } 934 } 935 936 @SuppressWarnings("unchecked") 937 private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull( 938 RequestPartitionId theRequestPartitionId, 939 String theSourceResourceName, 940 PathAndRef thePathAndRef, 941 ResourceTable theEntity, 942 Date theUpdateTime, 943 IIdType theNextId, 944 RequestDetails theRequest, 945 TransactionDetails theTransactionDetails) { 946 JpaPid resolvedResourceId = (JpaPid) theTransactionDetails.getResolvedResourceId(theNextId); 947 948 if (resolvedResourceId != null) { 949 String targetResourceType = theNextId.getResourceType(); 950 Long targetResourcePid = resolvedResourceId.getId(); 951 String targetResourceIdPart = theNextId.getIdPart(); 952 Long targetVersion = theNextId.getVersionIdPartAsLong(); 953 954 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 955 .setSourcePath(thePathAndRef.getPath()) 956 .setSourceResource(theEntity) 957 .setTargetResourceType(targetResourceType) 958 .setTargetResourcePid(targetResourcePid) 959 .setTargetResourceId(targetResourceIdPart) 960 .setUpdated(theUpdateTime) 961 .setTargetResourceVersion(targetVersion) 962 .setTargetResourcePartitionablePartitionId(resolvedResourceId.getPartitionablePartitionId()); 963 964 return ResourceLink.forLocalReference(params); 965 } 966 967 /* 968 * We keep a cache of resolved target resources. This is good since for some resource types, there 969 * are multiple search parameters that map to the same element path within a resource (e.g. 970 * Observation:patient and Observation.subject and we don't want to force a resolution of the 971 * target any more times than we have to. 972 */ 973 974 IResourceLookup<JpaPid> targetResource; 975 if (myPartitionSettings.isPartitioningEnabled()) { 976 if (myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) { 977 978 // Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED 979 IInterceptorBroadcaster compositeBroadcaster = 980 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 981 if (compositeBroadcaster.hasHooks(Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE)) { 982 CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails( 983 theRequestPartitionId, 984 theSourceResourceName, 985 thePathAndRef, 986 theRequest, 987 theTransactionDetails); 988 HookParams params = new HookParams(referenceDetails); 989 targetResource = (IResourceLookup<JpaPid>) compositeBroadcaster.callHooksAndReturnObject( 990 Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, params); 991 } else { 992 targetResource = myResourceLinkResolver.findTargetResource( 993 RequestPartitionId.allPartitions(), 994 theSourceResourceName, 995 thePathAndRef, 996 theRequest, 997 theTransactionDetails); 998 } 999 1000 } else { 1001 targetResource = myResourceLinkResolver.findTargetResource( 1002 theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails); 1003 } 1004 } else { 1005 targetResource = myResourceLinkResolver.findTargetResource( 1006 theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails); 1007 } 1008 1009 if (targetResource == null) { 1010 return null; 1011 } 1012 1013 String targetResourceType = targetResource.getResourceType(); 1014 Long targetResourcePid = targetResource.getPersistentId().getId(); 1015 String targetResourceIdPart = theNextId.getIdPart(); 1016 Long targetVersion = theNextId.getVersionIdPartAsLong(); 1017 1018 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 1019 .setSourcePath(thePathAndRef.getPath()) 1020 .setSourceResource(theEntity) 1021 .setTargetResourceType(targetResourceType) 1022 .setTargetResourcePid(targetResourcePid) 1023 .setTargetResourceId(targetResourceIdPart) 1024 .setUpdated(theUpdateTime) 1025 .setTargetResourceVersion(targetVersion) 1026 .setTargetResourcePartitionablePartitionId(targetResource.getPartitionId()); 1027 1028 return forLocalReference(params); 1029 } 1030 1031 private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) { 1032 RequestPartitionId targetRequestPartitionId = theRequestPartitionId; 1033 if (myPartitionSettings.isPartitioningEnabled() 1034 && myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) { 1035 targetRequestPartitionId = RequestPartitionId.allPartitions(); 1036 } 1037 return targetRequestPartitionId; 1038 } 1039 1040 private void populateResourceTable( 1041 Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) { 1042 for (BaseResourceIndexedSearchParam next : theParams) { 1043 if (next.getResourcePid() == null) { 1044 next.setResource(theResourceTable); 1045 } 1046 } 1047 } 1048 1049 private void populateResourceTableForComboParams( 1050 Collection<? extends IResourceIndexComboSearchParameter> theParams, ResourceTable theResourceTable) { 1051 for (IResourceIndexComboSearchParameter next : theParams) { 1052 if (next.getResource() == null) { 1053 next.setResource(theResourceTable); 1054 if (next instanceof BasePartitionable) { 1055 ((BasePartitionable) next).setPartitionId(theResourceTable.getPartitionId()); 1056 } 1057 } 1058 } 1059 } 1060 1061 @VisibleForTesting 1062 void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) { 1063 myInterceptorBroadcaster = theInterceptorBroadcaster; 1064 } 1065 1066 @Nonnull 1067 public List<String> extractParamValuesAsStrings( 1068 RuntimeSearchParam theActiveSearchParam, IBaseResource theResource) { 1069 return mySearchParamExtractor.extractParamValuesAsStrings(theActiveSearchParam, theResource); 1070 } 1071 1072 public void extractSearchParamComboUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { 1073 String resourceType = theEntity.getResourceType(); 1074 Set<ResourceIndexedComboStringUnique> comboUniques = 1075 mySearchParamExtractor.extractSearchParamComboUnique(resourceType, theParams); 1076 theParams.myComboStringUniques.addAll(comboUniques); 1077 populateResourceTableForComboParams(theParams.myComboStringUniques, theEntity); 1078 } 1079 1080 public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { 1081 String resourceType = theEntity.getResourceType(); 1082 Set<ResourceIndexedComboTokenNonUnique> comboNonUniques = 1083 mySearchParamExtractor.extractSearchParamComboNonUnique(resourceType, theParams); 1084 theParams.myComboTokenNonUnique.addAll(comboNonUniques); 1085 populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity); 1086 } 1087 1088 private boolean shouldStripVersionFromReferenceAtPath(String theSearchParamPath) { 1089 1090 if (!myContext.getParserOptions().isStripVersionsFromReferences()) { 1091 // all references allowed to have versions globally, so don't strip 1092 return false; 1093 } 1094 1095 // global setting is to strip versions, see if there's any exceptions configured for specific paths 1096 Set<String> pathsAllowedToHaveVersionedRefs = 1097 myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths(); 1098 1099 if (pathsAllowedToHaveVersionedRefs.contains(theSearchParamPath)) { 1100 // path exactly matches 1101 return false; 1102 } 1103 1104 // there are some search parameters using a where clause to index the element for a specific resource type, such 1105 // as "Provenance.target.where(resolve() is Patient)". We insert these in the ResourceLink table as well. 1106 // Such entries in the ResourceLink table should remain versioned if the element is allowed to be versioned. 1107 return pathsAllowedToHaveVersionedRefs.stream() 1108 .noneMatch(pathToKeepVersioned -> theSearchParamPath.matches( 1109 pathToKeepVersioned + "\\.where\\(resolve\\(\\) is [A-Z][a-zA-Z]*\\)")); 1110 } 1111 1112 /** 1113 * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)} 1114 * in order to use that method for extracting chained search parameter indexes both 1115 * from contained resources and from uplifted refchains. 1116 */ 1117 private interface IChainedSearchParameterExtractionStrategy { 1118 1119 /** 1120 * Which search parameters should be indexed for the resource target 1121 * at the given path. In other words if thePathAndRef contains 1122 * "Patient/123", then we could return a filter that only lets the 1123 * "name" and "gender" search params through if we only want those 1124 * two parameters to be indexed for the resolved Patient resource 1125 * with that ID. 1126 */ 1127 @Nonnull 1128 ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef); 1129 1130 /** 1131 * Actually fetch the resource at the given path, or return 1132 * {@literal null} if none can be found. 1133 */ 1134 @Nullable 1135 IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef); 1136 } 1137 1138 static void handleWarnings( 1139 RequestDetails theRequestDetails, 1140 IInterceptorBroadcaster theInterceptorBroadcaster, 1141 ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) { 1142 if (theSearchParamSet.getWarnings().isEmpty()) { 1143 return; 1144 } 1145 1146 // If extraction generated any warnings, broadcast an error 1147 IInterceptorBroadcaster compositeBroadcaster = 1148 CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails); 1149 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { 1150 for (String next : theSearchParamSet.getWarnings()) { 1151 StorageProcessingMessage messageHolder = new StorageProcessingMessage(); 1152 messageHolder.setMessage(next); 1153 HookParams params = new HookParams() 1154 .add(RequestDetails.class, theRequestDetails) 1155 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1156 .add(StorageProcessingMessage.class, messageHolder); 1157 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 1158 } 1159 } 1160 } 1161}