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