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