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