
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) continue; 441 442 // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite 443 // loops 444 if (theAlreadySeenResources.contains(targetResource)) { 445 continue; 446 } 447 448 ResourceIndexedSearchParams currParams = ResourceIndexedSearchParams.withSets(); 449 450 // 3.3 create indexes for the current contained resource 451 getExtractionUtil() 452 .extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex); 453 454 // 3.4 recurse to process any other contained resources referenced by this one 455 // Recursing is currently only allowed for contained resources and not 456 // uplifted refchains because the latter could potentially kill performance 457 // with the number of resource resolutions needed in order to handle 458 // a single write. Maybe in the future we could add caching to improve 459 // this 460 if (theRecurse) { 461 HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); 462 nextAlreadySeenResources.add(targetResource); 463 464 ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = 465 mySearchParamExtractor.extractResourceLinks(targetResource, theIndexOnContainedResources); 466 SearchParamExtractorService.handleWarnings( 467 theRequestDetails, myInterceptorBroadcaster, indexedReferences); 468 469 extractSearchIndexParametersForTargetResources( 470 theRequestDetails, 471 currParams, 472 theEntity, 473 nextAlreadySeenResources, 474 theTargetIndexingStrategy, 475 indexedReferences, 476 true, 477 theIndexOnContainedResources); 478 } 479 480 // 3.5 added reference name as a prefix for the contained resource if any 481 // e.g. for Observation.subject contained reference 482 // the SP_NAME = subject.family 483 currParams.updateSpnamePrefixForIndexOnUpliftedChain( 484 theEntity.getResourceType(), nextPathAndRef.getSearchParamName()); 485 486 // 3.6 merge to the mainParams 487 // NOTE: the spname prefix is different 488 mergeParams(currParams, theParams); 489 } 490 } 491 492 private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) { 493 for (IBaseResource resource : resources) { 494 String referenceString = reference.getReferenceElement().getValue(); 495 if (referenceString != null && referenceString.length() > 1) { 496 referenceString = referenceString.substring(1); 497 if (resource.getIdElement().getValue().equals(referenceString)) { 498 return resource; 499 } 500 } 501 } 502 return null; 503 } 504 505 private void mergeParams(ResourceIndexedSearchParams theSrcParams, ResourceIndexedSearchParams theTargetParams) { 506 507 theTargetParams.myNumberParams.addAll(theSrcParams.myNumberParams); 508 theTargetParams.myQuantityParams.addAll(theSrcParams.myQuantityParams); 509 theTargetParams.myQuantityNormalizedParams.addAll(theSrcParams.myQuantityNormalizedParams); 510 theTargetParams.myDateParams.addAll(theSrcParams.myDateParams); 511 theTargetParams.myUriParams.addAll(theSrcParams.myUriParams); 512 theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams); 513 theTargetParams.myStringParams.addAll(theSrcParams.myStringParams); 514 theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams); 515 theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams); 516 } 517 518 private void populateResourceTables(ResourceIndexedSearchParams theParams, ResourceTable theEntity) { 519 520 populateResourceTable(theParams.myNumberParams, theEntity); 521 populateResourceTable(theParams.myQuantityParams, theEntity); 522 populateResourceTable(theParams.myQuantityNormalizedParams, theEntity); 523 populateResourceTable(theParams.myDateParams, theEntity); 524 populateResourceTable(theParams.myUriParams, theEntity); 525 populateResourceTable(theParams.myTokenParams, theEntity); 526 populateResourceTable(theParams.myStringParams, theEntity); 527 populateResourceTable(theParams.myCoordsParams, theEntity); 528 } 529 530 @VisibleForTesting 531 public void setContext(FhirContext theContext) { 532 myContext = theContext; 533 } 534 535 private void extractResourceLinks( 536 RequestPartitionId theRequestPartitionId, 537 ResourceIndexedSearchParams theParams, 538 ResourceTable theEntity, 539 IBaseResource theResource, 540 TransactionDetails theTransactionDetails, 541 boolean theFailOnInvalidReference, 542 RequestDetails theRequest, 543 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 544 extractResourceLinks( 545 theRequestPartitionId, 546 ResourceIndexedSearchParams.withSets(), 547 theParams, 548 theEntity, 549 theResource, 550 theTransactionDetails, 551 theFailOnInvalidReference, 552 theRequest, 553 theIndexedReferences); 554 } 555 556 private void extractResourceLinks( 557 RequestPartitionId theRequestPartitionId, 558 ResourceIndexedSearchParams theExistingParams, 559 ResourceIndexedSearchParams theNewParams, 560 ResourceTable theEntity, 561 IBaseResource theResource, 562 TransactionDetails theTransactionDetails, 563 boolean theFailOnInvalidReference, 564 RequestDetails theRequest, 565 ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) { 566 String sourceResourceName = myContext.getResourceType(theResource); 567 568 for (PathAndRef nextPathAndRef : theIndexedReferences) { 569 if (nextPathAndRef.getRef() != null) { 570 if (nextPathAndRef.getRef().getReferenceElement().isLocal()) { 571 continue; 572 } 573 574 RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam( 575 sourceResourceName, 576 nextPathAndRef.getSearchParamName(), 577 ISearchParamRegistry.SearchParamLookupContextEnum.INDEX); 578 extractResourceLinks( 579 theRequestPartitionId, 580 theExistingParams, 581 theNewParams, 582 theEntity, 583 theTransactionDetails, 584 sourceResourceName, 585 searchParam, 586 nextPathAndRef, 587 theFailOnInvalidReference, 588 theRequest); 589 } 590 } 591 592 theEntity.setHasLinks(!theNewParams.myLinks.isEmpty()); 593 } 594 595 private void extractResourceLinks( 596 RequestPartitionId theRequestPartitionId, 597 ResourceIndexedSearchParams theExistingParams, 598 ResourceIndexedSearchParams theNewParams, 599 ResourceTable theEntity, 600 TransactionDetails theTransactionDetails, 601 String theSourceResourceName, 602 RuntimeSearchParam theRuntimeSearchParam, 603 PathAndRef thePathAndRef, 604 boolean theFailOnInvalidReference, 605 RequestDetails theRequest) { 606 IBaseReference nextReference = thePathAndRef.getRef(); 607 IIdType nextId = nextReference.getReferenceElement(); 608 String path = thePathAndRef.getPath(); 609 Date transactionDate = theTransactionDetails.getTransactionDate(); 610 611 /* 612 * This can only really happen if the DAO is being called 613 * programmatically with a Bundle (not through the FHIR REST API) 614 * but Smile does this 615 */ 616 if (nextId.isEmpty() && nextReference.getResource() != null) { 617 nextId = nextReference.getResource().getIdElement(); 618 } 619 620 if (nextId.hasVersionIdPart() && shouldStripVersionFromReferenceAtPath(thePathAndRef.getPath())) { 621 nextId = nextId.toVersionless(); 622 } 623 624 theNewParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName()); 625 626 boolean canonical = thePathAndRef.isCanonical(); 627 if (LogicalReferenceHelper.isLogicalReference(myStorageSettings, nextId) || canonical) { 628 String value = nextId.getValue(); 629 ResourceLink resourceLink = 630 ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, transactionDate); 631 if (theNewParams.myLinks.add(resourceLink)) { 632 ourLog.debug("Indexing remote resource reference URL: {}", nextId); 633 } 634 return; 635 } 636 637 String baseUrl = nextId.getBaseUrl(); 638 639 // If this is a conditional URL, the part after the question mark 640 // can include URLs (e.g. token system URLs) and these really confuse 641 // the IdType parser because a conditional URL isn't actually a valid 642 // FHIR ID. So in order to truly determine whether we're dealing with 643 // an absolute reference, we strip the query part and reparse 644 // the reference. 645 int questionMarkIndex = nextId.getValue().indexOf('?'); 646 if (questionMarkIndex != -1) { 647 IdType preQueryId = new IdType(nextId.getValue().substring(0, questionMarkIndex - 1)); 648 baseUrl = preQueryId.getBaseUrl(); 649 } 650 651 String typeString = nextId.getResourceType(); 652 if (isBlank(typeString)) { 653 String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - " 654 + nextId.getValue(); 655 if (theFailOnInvalidReference) { 656 throw new InvalidRequestException(Msg.code(505) + msg); 657 } else { 658 ourLog.debug(msg); 659 return; 660 } 661 } 662 RuntimeResourceDefinition resourceDefinition; 663 try { 664 resourceDefinition = myContext.getResourceDefinition(typeString); 665 } catch (DataFormatException e) { 666 String msg = "Invalid resource reference found at path[" + path 667 + "] - Resource type is unknown or not supported on this server - " + nextId.getValue(); 668 if (theFailOnInvalidReference) { 669 throw new InvalidRequestException(Msg.code(506) + msg); 670 } else { 671 ourLog.debug(msg); 672 return; 673 } 674 } 675 676 if (theRuntimeSearchParam.hasTargets()) { 677 if (!theRuntimeSearchParam.getTargets().contains(typeString)) { 678 return; 679 } 680 } 681 682 if (isNotBlank(baseUrl)) { 683 if (!myStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl) 684 && !myStorageSettings.isAllowExternalReferences()) { 685 String msg = myContext 686 .getLocalizer() 687 .getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue()); 688 throw new InvalidRequestException(Msg.code(507) + msg); 689 } else { 690 ResourceLink resourceLink = 691 ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, transactionDate); 692 if (theNewParams.myLinks.add(resourceLink)) { 693 ourLog.debug("Indexing remote resource reference URL: {}", nextId); 694 } 695 return; 696 } 697 } 698 699 Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass(); 700 String targetId = nextId.getIdPart(); 701 if (StringUtils.isBlank(targetId)) { 702 String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - " 703 + nextId.getValue(); 704 if (theFailOnInvalidReference) { 705 throw new InvalidRequestException(Msg.code(508) + msg); 706 } else { 707 ourLog.debug(msg); 708 return; 709 } 710 } 711 712 IIdType referenceElement = thePathAndRef.getRef().getReferenceElement(); 713 if (isBlank(referenceElement.getValue())) { 714 // it's an embedded element maybe; 715 // we need a valid referenceElement, becuase we 716 // resolve the resource by this value (and if we use "null", we can't resolve multiple values) 717 referenceElement = thePathAndRef.getRef().getResource().getIdElement(); 718 } 719 JpaPid resolvedTargetId = (JpaPid) theTransactionDetails.getResolvedResourceId(referenceElement); 720 ResourceLink resourceLink; 721 722 Long targetVersionId = nextId.getVersionIdPartAsLong(); 723 if (resolvedTargetId != null 724 && myRequestPartitionHelperSvc.isPidPartitionWithinRequestPartition( 725 theRequestPartitionId, resolvedTargetId)) { 726 727 /* 728 * If we have already resolved the given reference within this transaction, we don't 729 * need to resolve it again 730 */ 731 myResourceLinkResolver.validateTypeOrThrowException(type); 732 733 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 734 .setSourcePath(thePathAndRef.getPath()) 735 .setSourceResource(theEntity) 736 .setTargetResourceType(typeString) 737 .setTargetResourcePid(resolvedTargetId.getId()) 738 .setTargetResourceId(targetId) 739 .setUpdated(transactionDate) 740 .setTargetResourceVersion(targetVersionId) 741 .setTargetResourcePartitionablePartitionId(resolvedTargetId.getPartitionablePartitionId()); 742 743 resourceLink = forLocalReference(params); 744 745 } else if (theFailOnInvalidReference) { 746 747 /* 748 * The reference points to another resource, so let's look it up. We need to do this 749 * since the target may be a forced ID, but also so that we can throw an exception 750 * if the reference is invalid 751 */ 752 myResourceLinkResolver.validateTypeOrThrowException(type); 753 754 /* 755 * We need to obtain a resourceLink out of the provided {@literal thePathAndRef}. In the case 756 * where we are updating a resource that already has resourceLinks (stored in {@literal theExistingParams.getResourceLinks()}), 757 * let's try to match thePathAndRef to an already existing resourceLink to avoid the 758 * very expensive operation of creating a resourceLink that would end up being exactly the same 759 * one we already have. 760 */ 761 Optional<ResourceLink> optionalResourceLink = 762 findMatchingResourceLink(thePathAndRef, theExistingParams.getResourceLinks()); 763 if (optionalResourceLink.isPresent()) { 764 resourceLink = optionalResourceLink.get(); 765 } else { 766 resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull( 767 theRequestPartitionId, 768 theSourceResourceName, 769 thePathAndRef, 770 theEntity, 771 transactionDate, 772 nextId, 773 theRequest, 774 theTransactionDetails); 775 } 776 777 if (resourceLink == null) { 778 return; 779 } else { 780 // Cache the outcome in the current transaction in case there are more references 781 JpaPid persistentId = 782 JpaPid.fromId(resourceLink.getTargetResourcePid(), resourceLink.getTargetResourcePartitionId()); 783 persistentId.setPartitionablePartitionId(PartitionablePartitionId.with( 784 resourceLink.getTargetResourcePartitionId(), resourceLink.getTargetResourcePartitionDate())); 785 theTransactionDetails.addResolvedResourceId(referenceElement, persistentId); 786 } 787 788 } else { 789 790 /* 791 * Just assume the reference is valid. This is used for in-memory matching since there 792 * is no expectation of a database in this situation 793 */ 794 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 795 .setSourcePath(thePathAndRef.getPath()) 796 .setSourceResource(theEntity) 797 .setTargetResourceType(typeString) 798 .setTargetResourceId(targetId) 799 .setUpdated(transactionDate) 800 .setTargetResourceVersion(targetVersionId); 801 802 resourceLink = forLocalReference(params); 803 } 804 805 theNewParams.myLinks.add(resourceLink); 806 } 807 808 private Optional<ResourceLink> findMatchingResourceLink( 809 PathAndRef thePathAndRef, Collection<ResourceLink> theResourceLinks) { 810 IIdType referenceElement = thePathAndRef.getRef().getReferenceElement(); 811 List<ResourceLink> resourceLinks = new ArrayList<>(theResourceLinks); 812 for (ResourceLink resourceLink : resourceLinks) { 813 814 // comparing the searchParam path ex: Group.member.entity 815 boolean hasMatchingSearchParamPath = 816 StringUtils.equals(resourceLink.getSourcePath(), thePathAndRef.getPath()); 817 818 boolean hasMatchingResourceType = 819 StringUtils.equals(resourceLink.getTargetResourceType(), referenceElement.getResourceType()); 820 821 boolean hasMatchingResourceId = 822 StringUtils.equals(resourceLink.getTargetResourceId(), referenceElement.getIdPart()); 823 824 boolean hasMatchingResourceVersion = myContext.getParserOptions().isStripVersionsFromReferences() 825 || referenceElement.getVersionIdPartAsLong() == null 826 || referenceElement.getVersionIdPartAsLong().equals(resourceLink.getTargetResourceVersion()); 827 828 if (hasMatchingSearchParamPath 829 && hasMatchingResourceType 830 && hasMatchingResourceId 831 && hasMatchingResourceVersion) { 832 return Optional.of(resourceLink); 833 } 834 } 835 836 return Optional.empty(); 837 } 838 839 private void extractResourceLinksForContainedResources( 840 RequestPartitionId theRequestPartitionId, 841 ResourceIndexedSearchParams theParams, 842 ResourceTable theEntity, 843 IBaseResource theResource, 844 TransactionDetails theTransactionDetails, 845 boolean theFailOnInvalidReference, 846 RequestDetails theRequest) { 847 848 FhirTerser terser = myContext.newTerser(); 849 850 // 1. get all contained resources 851 Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false); 852 853 extractResourceLinksForContainedResources( 854 theRequestPartitionId, 855 theParams, 856 theEntity, 857 theResource, 858 theTransactionDetails, 859 theFailOnInvalidReference, 860 theRequest, 861 containedResources, 862 new HashSet<>()); 863 } 864 865 private void extractResourceLinksForContainedResources( 866 RequestPartitionId theRequestPartitionId, 867 ResourceIndexedSearchParams theParams, 868 ResourceTable theEntity, 869 IBaseResource theResource, 870 TransactionDetails theTransactionDetails, 871 boolean theFailOnInvalidReference, 872 RequestDetails theRequest, 873 Collection<IBaseResource> theContainedResources, 874 Collection<IBaseResource> theAlreadySeenResources) { 875 876 // 2. Find referenced search parameters 877 ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = 878 mySearchParamExtractor.extractResourceLinks(theResource, true); 879 880 String spNamePrefix; 881 ResourceIndexedSearchParams currParams; 882 // 3. for each referenced search parameter, create an index 883 for (PathAndRef nextPathAndRef : referencedSearchParamSet) { 884 885 // 3.1 get the search parameter name as spname prefix 886 spNamePrefix = nextPathAndRef.getSearchParamName(); 887 888 if (spNamePrefix == null || nextPathAndRef.getRef() == null) continue; 889 890 // 3.2 find the contained resource 891 IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef()); 892 if (containedResource == null) continue; 893 894 // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite 895 // loops 896 if (theAlreadySeenResources.contains(containedResource)) { 897 continue; 898 } 899 900 currParams = ResourceIndexedSearchParams.withSets(); 901 902 // 3.3 create indexes for the current contained resource 903 ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences = 904 mySearchParamExtractor.extractResourceLinks(containedResource, true); 905 extractResourceLinks( 906 theRequestPartitionId, 907 currParams, 908 theEntity, 909 containedResource, 910 theTransactionDetails, 911 theFailOnInvalidReference, 912 theRequest, 913 indexedReferences); 914 915 // 3.4 recurse to process any other contained resources referenced by this one 916 if (myStorageSettings.isIndexOnContainedResourcesRecursively()) { 917 HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources); 918 nextAlreadySeenResources.add(containedResource); 919 extractResourceLinksForContainedResources( 920 theRequestPartitionId, 921 currParams, 922 theEntity, 923 containedResource, 924 theTransactionDetails, 925 theFailOnInvalidReference, 926 theRequest, 927 theContainedResources, 928 nextAlreadySeenResources); 929 } 930 931 // 3.4 added reference name as a prefix for the contained resource if any 932 // e.g. for Observation.subject contained reference 933 // the SP_NAME = subject.family 934 currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath()); 935 936 // 3.5 merge to the mainParams 937 // NOTE: the spname prefix is different 938 theParams.getResourceLinks().addAll(currParams.getResourceLinks()); 939 } 940 } 941 942 @SuppressWarnings("unchecked") 943 private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull( 944 RequestPartitionId theRequestPartitionId, 945 String theSourceResourceName, 946 PathAndRef thePathAndRef, 947 ResourceTable theEntity, 948 Date theUpdateTime, 949 IIdType theNextId, 950 RequestDetails theRequest, 951 TransactionDetails theTransactionDetails) { 952 JpaPid resolvedResourceId = (JpaPid) theTransactionDetails.getResolvedResourceId(theNextId); 953 954 if (resolvedResourceId != null 955 && myRequestPartitionHelperSvc.isPidPartitionWithinRequestPartition( 956 theRequestPartitionId, resolvedResourceId)) { 957 String targetResourceType = theNextId.getResourceType(); 958 Long targetResourcePid = resolvedResourceId.getId(); 959 String targetResourceIdPart = theNextId.getIdPart(); 960 Long targetVersion = theNextId.getVersionIdPartAsLong(); 961 962 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 963 .setSourcePath(thePathAndRef.getPath()) 964 .setSourceResource(theEntity) 965 .setTargetResourceType(targetResourceType) 966 .setTargetResourcePid(targetResourcePid) 967 .setTargetResourceId(targetResourceIdPart) 968 .setUpdated(theUpdateTime) 969 .setTargetResourceVersion(targetVersion) 970 .setTargetResourcePartitionablePartitionId(resolvedResourceId.getPartitionablePartitionId()); 971 972 return ResourceLink.forLocalReference(params); 973 } 974 975 /* 976 * We keep a cache of resolved target resources. This is good since for some resource types, there 977 * are multiple search parameters that map to the same element path within a resource (e.g. 978 * Observation:patient and Observation.subject and we don't want to force a resolution of the 979 * target any more times than we have to. 980 */ 981 982 IResourceLookup<JpaPid> targetResource; 983 if (myPartitionSettings.isPartitioningEnabled()) { 984 if (myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) { 985 986 // Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED 987 IInterceptorBroadcaster compositeBroadcaster = 988 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 989 if (compositeBroadcaster.hasHooks(Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE)) { 990 CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails( 991 theRequestPartitionId, 992 theSourceResourceName, 993 thePathAndRef, 994 theRequest, 995 theTransactionDetails); 996 HookParams params = new HookParams(referenceDetails); 997 targetResource = (IResourceLookup<JpaPid>) compositeBroadcaster.callHooksAndReturnObject( 998 Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, params); 999 } else { 1000 RequestPartitionId requestPartitionId = RequestPartitionId.allPartitions(); 1001 if (resolvedResourceId != null) { 1002 requestPartitionId = RequestPartitionId.fromPartitionId(resolvedResourceId.getPartitionId()); 1003 } 1004 targetResource = myResourceLinkResolver.findTargetResource( 1005 requestPartitionId, 1006 theSourceResourceName, 1007 thePathAndRef, 1008 theRequest, 1009 theTransactionDetails); 1010 } 1011 1012 } else { 1013 targetResource = myResourceLinkResolver.findTargetResource( 1014 theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails); 1015 } 1016 } else { 1017 targetResource = myResourceLinkResolver.findTargetResource( 1018 theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails); 1019 } 1020 1021 if (targetResource == null) { 1022 return null; 1023 } 1024 1025 String targetResourceType = targetResource.getResourceType(); 1026 Long targetResourcePid = targetResource.getPersistentId().getId(); 1027 String targetResourceIdPart = theNextId.getIdPart(); 1028 Long targetVersion = theNextId.getVersionIdPartAsLong(); 1029 1030 ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance() 1031 .setSourcePath(thePathAndRef.getPath()) 1032 .setSourceResource(theEntity) 1033 .setTargetResourceType(targetResourceType) 1034 .setTargetResourcePid(targetResourcePid) 1035 .setTargetResourceId(targetResourceIdPart) 1036 .setUpdated(theUpdateTime) 1037 .setTargetResourceVersion(targetVersion) 1038 .setTargetResourcePartitionablePartitionId(targetResource.getPartitionId()); 1039 1040 return forLocalReference(params); 1041 } 1042 1043 private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) { 1044 RequestPartitionId targetRequestPartitionId = theRequestPartitionId; 1045 if (myPartitionSettings.isPartitioningEnabled() 1046 && myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) { 1047 targetRequestPartitionId = RequestPartitionId.allPartitions(); 1048 } 1049 return targetRequestPartitionId; 1050 } 1051 1052 private void populateResourceTable( 1053 Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) { 1054 for (BaseResourceIndexedSearchParam next : theParams) { 1055 if (next.getResourcePid() == null) { 1056 next.setResource(theResourceTable); 1057 } 1058 } 1059 } 1060 1061 private void populateResourceTableForComboParams( 1062 Collection<? extends IResourceIndexComboSearchParameter> theParams, ResourceTable theResourceTable) { 1063 for (IResourceIndexComboSearchParameter next : theParams) { 1064 if (next.getResource() == null) { 1065 next.setResource(theResourceTable); 1066 if (next instanceof BasePartitionable) { 1067 ((BasePartitionable) next).setPartitionId(theResourceTable.getPartitionId()); 1068 } 1069 } 1070 } 1071 } 1072 1073 @VisibleForTesting 1074 void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) { 1075 myInterceptorBroadcaster = theInterceptorBroadcaster; 1076 } 1077 1078 @Nonnull 1079 public List<String> extractParamValuesAsStrings( 1080 RuntimeSearchParam theActiveSearchParam, IBaseResource theResource) { 1081 return mySearchParamExtractor.extractParamValuesAsStrings(theActiveSearchParam, theResource); 1082 } 1083 1084 public void extractSearchParamComboUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { 1085 String resourceType = theEntity.getResourceType(); 1086 Set<ResourceIndexedComboStringUnique> comboUniques = 1087 mySearchParamExtractor.extractSearchParamComboUnique(resourceType, theParams); 1088 theParams.myComboStringUniques.addAll(comboUniques); 1089 populateResourceTableForComboParams(theParams.myComboStringUniques, theEntity); 1090 } 1091 1092 public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) { 1093 String resourceType = theEntity.getResourceType(); 1094 Set<ResourceIndexedComboTokenNonUnique> comboNonUniques = 1095 mySearchParamExtractor.extractSearchParamComboNonUnique(resourceType, theParams); 1096 theParams.myComboTokenNonUnique.addAll(comboNonUniques); 1097 populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity); 1098 } 1099 1100 private boolean shouldStripVersionFromReferenceAtPath(String theSearchParamPath) { 1101 1102 if (!myContext.getParserOptions().isStripVersionsFromReferences()) { 1103 // all references allowed to have versions globally, so don't strip 1104 return false; 1105 } 1106 1107 // global setting is to strip versions, see if there's any exceptions configured for specific paths 1108 Set<String> pathsAllowedToHaveVersionedRefs = 1109 myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths(); 1110 1111 if (pathsAllowedToHaveVersionedRefs.contains(theSearchParamPath)) { 1112 // path exactly matches 1113 return false; 1114 } 1115 1116 // there are some search parameters using a where clause to index the element for a specific resource type, such 1117 // as "Provenance.target.where(resolve() is Patient)". We insert these in the ResourceLink table as well. 1118 // Such entries in the ResourceLink table should remain versioned if the element is allowed to be versioned. 1119 return pathsAllowedToHaveVersionedRefs.stream() 1120 .noneMatch(pathToKeepVersioned -> theSearchParamPath.matches( 1121 pathToKeepVersioned + "\\.where\\(resolve\\(\\) is [A-Z][a-zA-Z]*\\)")); 1122 } 1123 1124 /** 1125 * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)} 1126 * in order to use that method for extracting chained search parameter indexes both 1127 * from contained resources and from uplifted refchains. 1128 */ 1129 private interface IChainedSearchParameterExtractionStrategy { 1130 1131 /** 1132 * Which search parameters should be indexed for the resource target 1133 * at the given path. In other words if thePathAndRef contains 1134 * "Patient/123", then we could return a filter that only lets the 1135 * "name" and "gender" search params through if we only want those 1136 * two parameters to be indexed for the resolved Patient resource 1137 * with that ID. 1138 */ 1139 @Nonnull 1140 ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef); 1141 1142 /** 1143 * Actually fetch the resource at the given path, or return 1144 * {@literal null} if none can be found. 1145 */ 1146 @Nullable 1147 IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef); 1148 } 1149 1150 static void handleWarnings( 1151 RequestDetails theRequestDetails, 1152 IInterceptorBroadcaster theInterceptorBroadcaster, 1153 ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) { 1154 if (theSearchParamSet.getWarnings().isEmpty()) { 1155 return; 1156 } 1157 1158 // If extraction generated any warnings, broadcast an error 1159 IInterceptorBroadcaster compositeBroadcaster = 1160 CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails); 1161 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { 1162 for (String next : theSearchParamSet.getWarnings()) { 1163 StorageProcessingMessage messageHolder = new StorageProcessingMessage(); 1164 messageHolder.setMessage(next); 1165 HookParams params = new HookParams() 1166 .add(RequestDetails.class, theRequestDetails) 1167 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1168 .add(StorageProcessingMessage.class, messageHolder); 1169 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 1170 } 1171 } 1172 } 1173}