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