001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.search; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.api.HookParams; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; 027import ca.uhn.fhir.interceptor.model.RequestPartitionId; 028import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 029import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 030import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; 031import ca.uhn.fhir.jpa.dao.HistoryBuilder; 032import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory; 033import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 034import ca.uhn.fhir.jpa.dao.ISearchBuilder; 035import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; 036import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 037import ca.uhn.fhir.jpa.entity.Search; 038import ca.uhn.fhir.jpa.entity.SearchTypeEnum; 039import ca.uhn.fhir.jpa.model.dao.JpaPid; 040import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 041import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 042import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 043import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; 044import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum; 045import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 046import ca.uhn.fhir.jpa.util.MemoryCacheService; 047import ca.uhn.fhir.jpa.util.QueryParameterUtils; 048import ca.uhn.fhir.model.primitive.InstantDt; 049import ca.uhn.fhir.rest.api.server.IBundleProvider; 050import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 051import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 052import ca.uhn.fhir.rest.api.server.RequestDetails; 053import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 054import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 055import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil; 056import ca.uhn.fhir.rest.server.method.ResponsePage; 057import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 058import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 059import com.google.common.annotations.VisibleForTesting; 060import jakarta.annotation.Nonnull; 061import jakarta.persistence.EntityManager; 062import jakarta.persistence.PersistenceContext; 063import org.hl7.fhir.instance.model.api.IBaseResource; 064import org.slf4j.Logger; 065import org.slf4j.LoggerFactory; 066import org.springframework.beans.factory.annotation.Autowired; 067 068import java.util.ArrayList; 069import java.util.Collections; 070import java.util.List; 071import java.util.Optional; 072import java.util.Set; 073import java.util.function.Function; 074 075public class PersistedJpaBundleProvider implements IBundleProvider { 076 077 private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaBundleProvider.class); 078 079 /* 080 * Autowired fields 081 */ 082 protected final RequestDetails myRequest; 083 084 @Autowired 085 protected HapiTransactionService myTxService; 086 087 @PersistenceContext 088 private EntityManager myEntityManager; 089 090 @Autowired 091 private IInterceptorBroadcaster myInterceptorBroadcaster; 092 093 @Autowired 094 private SearchBuilderFactory<JpaPid> mySearchBuilderFactory; 095 096 @Autowired 097 private HistoryBuilderFactory myHistoryBuilderFactory; 098 099 @Autowired 100 private DaoRegistry myDaoRegistry; 101 102 @Autowired 103 private FhirContext myContext; 104 105 @Autowired 106 private ISearchCoordinatorSvc<JpaPid> mySearchCoordinatorSvc; 107 108 @Autowired 109 private ISearchCacheSvc mySearchCacheSvc; 110 111 @Autowired 112 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 113 114 @Autowired 115 private JpaStorageSettings myStorageSettings; 116 117 @Autowired 118 private MemoryCacheService myMemoryCacheService; 119 120 @Autowired 121 private IJpaStorageResourceParser myJpaStorageResourceParser; 122 /* 123 * Non autowired fields (will be different for every instance 124 * of this class, since it's a prototype 125 */ 126 private Search mySearchEntity; 127 private final String myUuid; 128 private SearchCacheStatusEnum myCacheStatus; 129 private RequestPartitionId myRequestPartitionId; 130 131 /** 132 * Constructor 133 */ 134 public PersistedJpaBundleProvider(RequestDetails theRequest, String theSearchUuid) { 135 myRequest = theRequest; 136 myUuid = theSearchUuid; 137 } 138 139 /** 140 * Constructor 141 */ 142 public PersistedJpaBundleProvider(RequestDetails theRequest, Search theSearch) { 143 myRequest = theRequest; 144 mySearchEntity = theSearch; 145 myUuid = theSearch.getUuid(); 146 } 147 148 @VisibleForTesting 149 public void setRequestPartitionHelperSvcForUnitTest(IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { 150 myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; 151 } 152 153 @VisibleForTesting 154 public Search getSearchEntityForTesting() { 155 return getSearchEntity(); 156 } 157 158 protected Search getSearchEntity() { 159 return mySearchEntity; 160 } 161 162 // Note: Leave as protected, HSPC depends on this 163 @SuppressWarnings("WeakerAccess") 164 protected void setSearchEntity(Search theSearchEntity) { 165 mySearchEntity = theSearchEntity; 166 } 167 168 /** 169 * Perform a history search 170 */ 171 private List<IBaseResource> doHistoryInTransaction(Integer theOffset, int theFromIndex, int theToIndex) { 172 173 HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder( 174 mySearchEntity.getResourceType(), 175 mySearchEntity.getResourceId(), 176 mySearchEntity.getLastUpdatedLow(), 177 mySearchEntity.getLastUpdatedHigh()); 178 179 RequestPartitionId partitionId = getRequestPartitionId(); 180 List<ResourceHistoryTable> results = historyBuilder.fetchEntities( 181 partitionId, theOffset, theFromIndex, theToIndex, mySearchEntity.getHistorySearchStyle()); 182 183 List<IBaseResource> retVal = new ArrayList<>(); 184 for (ResourceHistoryTable next : results) { 185 BaseHasResource resource; 186 resource = next; 187 188 retVal.add(myJpaStorageResourceParser.toResource(resource, true)); 189 } 190 191 IInterceptorBroadcaster compositeBroadcaster = 192 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, myRequest); 193 194 // Interceptor call: STORAGE_PREACCESS_RESOURCES 195 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 196 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal); 197 HookParams params = new HookParams() 198 .add(IPreResourceAccessDetails.class, accessDetails) 199 .add(RequestDetails.class, myRequest) 200 .addIfMatchesType(ServletRequestDetails.class, myRequest); 201 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 202 203 for (int i = retVal.size() - 1; i >= 0; i--) { 204 if (accessDetails.isDontReturnResourceAtIndex(i)) { 205 retVal.remove(i); 206 } 207 } 208 } 209 210 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 211 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 212 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal); 213 HookParams params = new HookParams() 214 .add(IPreResourceShowDetails.class, showDetails) 215 .add(RequestDetails.class, myRequest) 216 .addIfMatchesType(ServletRequestDetails.class, myRequest); 217 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 218 retVal = showDetails.toList(); 219 } 220 221 return retVal; 222 } 223 224 @Nonnull 225 protected final RequestPartitionId getRequestPartitionId() { 226 if (myRequestPartitionId == null) { 227 ReadPartitionIdRequestDetails details; 228 if (mySearchEntity == null) { 229 details = ReadPartitionIdRequestDetails.forSearchUuid(myUuid); 230 } else if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) { 231 details = ReadPartitionIdRequestDetails.forHistory(mySearchEntity.getResourceType(), null); 232 } else { 233 SearchParameterMap params = 234 mySearchEntity.getSearchParameterMap().orElse(null); 235 details = ReadPartitionIdRequestDetails.forSearchType(mySearchEntity.getResourceType(), params, null); 236 } 237 myRequestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(myRequest, details); 238 } 239 return myRequestPartitionId; 240 } 241 242 public void setRequestPartitionId(RequestPartitionId theRequestPartitionId) { 243 myRequestPartitionId = theRequestPartitionId; 244 } 245 246 protected List<IBaseResource> doSearchOrEverything( 247 final int theFromIndex, 248 final int theToIndex, 249 @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) { 250 if (mySearchEntity.getTotalCount() != null && mySearchEntity.getNumFound() <= 0) { 251 // No resources to fetch (e.g. we did a _summary=count search) 252 return Collections.emptyList(); 253 } 254 String resourceName = mySearchEntity.getResourceType(); 255 Class<? extends IBaseResource> resourceType = 256 myContext.getResourceDefinition(resourceName).getImplementingClass(); 257 258 final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(resourceName, resourceType); 259 260 RequestPartitionId requestPartitionId = getRequestPartitionId(); 261 // we request 1 more resource than we need 262 // this is so we can be sure of when we hit the last page 263 // (when doing offset searches) 264 final List<JpaPid> pidsSubList = mySearchCoordinatorSvc.getResources( 265 myUuid, theFromIndex, theToIndex + 1, myRequest, requestPartitionId); 266 // max list size should be either the entire list, or from - to length 267 int maxSize = Math.min(theToIndex - theFromIndex, pidsSubList.size()); 268 theResponsePageBuilder.setTotalRequestedResourcesFetched(pidsSubList.size()); 269 270 List<JpaPid> firstBatchOfPids = pidsSubList.subList(0, maxSize); 271 List<IBaseResource> resources = myTxService 272 .withRequest(myRequest) 273 .withRequestPartitionId(requestPartitionId) 274 .execute(() -> { 275 return toResourceList(sb, firstBatchOfPids, theResponsePageBuilder); 276 }); 277 278 return resources; 279 } 280 281 /** 282 * Returns false if the entity can't be found 283 */ 284 public boolean ensureSearchEntityLoaded() { 285 if (mySearchEntity == null) { 286 Optional<Search> searchOpt = myTxService 287 .withRequest(myRequest) 288 .withRequestPartitionId(myRequestPartitionId) 289 .execute(() -> mySearchCacheSvc.fetchByUuid(myUuid, myRequestPartitionId)); 290 if (!searchOpt.isPresent()) { 291 return false; 292 } 293 294 setSearchEntity(searchOpt.get()); 295 296 ourLog.trace( 297 "Retrieved search with version {} and total {}", 298 mySearchEntity.getVersion(), 299 mySearchEntity.getTotalCount()); 300 301 return true; 302 } 303 304 if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) { 305 if (mySearchEntity.getTotalCount() == null) { 306 calculateHistoryCount(); 307 } 308 } 309 310 return true; 311 } 312 313 /** 314 * Note that this method is called outside a DB transaction, and uses a loading cache 315 * (assuming the default {@literal COUNT_CACHED} mode) so this effectively throttles 316 * access to the database by preventing multiple concurrent DB calls for an expensive 317 * count operation. 318 */ 319 private void calculateHistoryCount() { 320 MemoryCacheService.HistoryCountKey key; 321 if (mySearchEntity.getResourceId() != null) { 322 key = MemoryCacheService.HistoryCountKey.forInstance(mySearchEntity.getResourceId()); 323 } else if (mySearchEntity.getResourceType() != null) { 324 key = MemoryCacheService.HistoryCountKey.forType(mySearchEntity.getResourceType()); 325 } else { 326 key = MemoryCacheService.HistoryCountKey.forSystem(); 327 } 328 329 Function<MemoryCacheService.HistoryCountKey, Integer> supplier = k -> myTxService 330 .withRequest(myRequest) 331 .withRequestPartitionId(getRequestPartitionId()) 332 .execute(() -> { 333 HistoryBuilder historyBuilder = myHistoryBuilderFactory.newHistoryBuilder( 334 mySearchEntity.getResourceType(), 335 mySearchEntity.getResourceId(), 336 mySearchEntity.getLastUpdatedLow(), 337 mySearchEntity.getLastUpdatedHigh()); 338 Long count = historyBuilder.fetchCount(getRequestPartitionId()); 339 return count.intValue(); 340 }); 341 342 boolean haveOffset = mySearchEntity.getLastUpdatedLow() != null || mySearchEntity.getLastUpdatedHigh() != null; 343 344 switch (myStorageSettings.getHistoryCountMode()) { 345 case COUNT_ACCURATE: { 346 int count = supplier.apply(key); 347 mySearchEntity.setTotalCount(count); 348 break; 349 } 350 case CACHED_ONLY_WITHOUT_OFFSET: { 351 if (!haveOffset) { 352 int count = myMemoryCacheService.get(MemoryCacheService.CacheEnum.HISTORY_COUNT, key, supplier); 353 mySearchEntity.setTotalCount(count); 354 } 355 break; 356 } 357 case COUNT_DISABLED: { 358 break; 359 } 360 } 361 } 362 363 @Override 364 public InstantDt getPublished() { 365 ensureSearchEntityLoaded(); 366 return new InstantDt(mySearchEntity.getCreated()); 367 } 368 369 @Nonnull 370 @Override 371 public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { 372 return getResources(theFromIndex, theToIndex, new ResponsePage.ResponsePageBuilder()); 373 } 374 375 @Override 376 public List<IBaseResource> getResources( 377 int theFromIndex, int theToIndex, @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) { 378 boolean entityLoaded = ensureSearchEntityLoaded(); 379 assert entityLoaded; 380 assert mySearchEntity != null; 381 assert mySearchEntity.getSearchType() != null; 382 383 switch (mySearchEntity.getSearchType()) { 384 case HISTORY: 385 return myTxService 386 .withRequest(myRequest) 387 .withRequestPartitionId(getRequestPartitionId()) 388 .execute(() -> doHistoryInTransaction(mySearchEntity.getOffset(), theFromIndex, theToIndex)); 389 case SEARCH: 390 case EVERYTHING: 391 default: 392 List<IBaseResource> retVal = doSearchOrEverything(theFromIndex, theToIndex, theResponsePageBuilder); 393 /* 394 * If we got fewer resources back than we asked for, it's possible that the search 395 * completed. If that's the case, the cached version of the search entity is probably 396 * no longer valid so let's force a reload if it gets asked for again (most likely 397 * because someone is calling size() on us) 398 */ 399 if (retVal.size() < theToIndex - theFromIndex) { 400 mySearchEntity = null; 401 } 402 return retVal; 403 } 404 } 405 406 @Override 407 public String getUuid() { 408 return myUuid; 409 } 410 411 public SearchCacheStatusEnum getCacheStatus() { 412 return myCacheStatus; 413 } 414 415 void setCacheStatus(SearchCacheStatusEnum theSearchCacheStatusEnum) { 416 myCacheStatus = theSearchCacheStatusEnum; 417 } 418 419 @Override 420 public Integer preferredPageSize() { 421 ensureSearchEntityLoaded(); 422 return mySearchEntity.getPreferredPageSize(); 423 } 424 425 public void setContext(FhirContext theContext) { 426 myContext = theContext; 427 } 428 429 public void setEntityManager(EntityManager theEntityManager) { 430 myEntityManager = theEntityManager; 431 } 432 433 @VisibleForTesting 434 public void setSearchCoordinatorSvcForUnitTest(ISearchCoordinatorSvc theSearchCoordinatorSvc) { 435 mySearchCoordinatorSvc = theSearchCoordinatorSvc; 436 } 437 438 @VisibleForTesting 439 public void setTxServiceForUnitTest(HapiTransactionService theTxManager) { 440 myTxService = theTxManager; 441 } 442 443 @Override 444 public Integer size() { 445 ensureSearchEntityLoaded(); 446 QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity); 447 448 Integer size = mySearchEntity.getTotalCount(); 449 if (size != null) { 450 return Math.max(0, size); 451 } 452 453 if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) { 454 return null; 455 } else { 456 return mySearchCoordinatorSvc 457 .getSearchTotal(myUuid, myRequest, myRequestPartitionId) 458 .orElse(null); 459 } 460 } 461 462 protected boolean hasIncludes() { 463 ensureSearchEntityLoaded(); 464 return !mySearchEntity.getIncludes().isEmpty(); 465 } 466 467 // Note: Leave as protected, HSPC depends on this 468 @SuppressWarnings("WeakerAccess") 469 protected List<IBaseResource> toResourceList( 470 ISearchBuilder theSearchBuilder, 471 List<JpaPid> thePids, 472 ResponsePage.ResponsePageBuilder theResponsePageBuilder) { 473 List<JpaPid> includedPidList = new ArrayList<>(); 474 if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) { 475 Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage(); 476 477 // Load non-iterate _revincludes 478 Set<JpaPid> nonIterateRevIncludedPids = theSearchBuilder.loadIncludes( 479 myContext, 480 myEntityManager, 481 thePids, 482 mySearchEntity.toRevIncludesList(false), 483 true, 484 mySearchEntity.getLastUpdated(), 485 myUuid, 486 myRequest, 487 maxIncludes); 488 if (maxIncludes != null) { 489 maxIncludes -= nonIterateRevIncludedPids.size(); 490 } 491 thePids.addAll(nonIterateRevIncludedPids); 492 includedPidList.addAll(nonIterateRevIncludedPids); 493 494 // Load non-iterate _includes 495 Set<JpaPid> nonIterateIncludedPids = theSearchBuilder.loadIncludes( 496 myContext, 497 myEntityManager, 498 thePids, 499 mySearchEntity.toIncludesList(false), 500 false, 501 mySearchEntity.getLastUpdated(), 502 myUuid, 503 myRequest, 504 maxIncludes); 505 if (maxIncludes != null) { 506 maxIncludes -= nonIterateIncludedPids.size(); 507 } 508 thePids.addAll(nonIterateIncludedPids); 509 includedPidList.addAll(nonIterateIncludedPids); 510 511 // Load iterate _revinclude 512 Set<JpaPid> iterateRevIncludedPids = theSearchBuilder.loadIncludes( 513 myContext, 514 myEntityManager, 515 thePids, 516 mySearchEntity.toRevIncludesList(true), 517 true, 518 mySearchEntity.getLastUpdated(), 519 myUuid, 520 myRequest, 521 maxIncludes); 522 if (maxIncludes != null) { 523 maxIncludes -= iterateRevIncludedPids.size(); 524 } 525 thePids.addAll(iterateRevIncludedPids); 526 includedPidList.addAll(iterateRevIncludedPids); 527 528 // Load iterate _includes 529 Set<JpaPid> iterateIncludedPids = theSearchBuilder.loadIncludes( 530 myContext, 531 myEntityManager, 532 thePids, 533 mySearchEntity.toIncludesList(true), 534 false, 535 mySearchEntity.getLastUpdated(), 536 myUuid, 537 myRequest, 538 maxIncludes); 539 thePids.addAll(iterateIncludedPids); 540 includedPidList.addAll(iterateIncludedPids); 541 } 542 543 // Execute the query and make sure we return distinct results 544 List<IBaseResource> resources = new ArrayList<>(); 545 theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest); 546 547 // we will send the resource list to our interceptors 548 // this can (potentially) change the results being returned. 549 int precount = resources.size(); 550 resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster); 551 // we only care about omitted results from this page 552 theResponsePageBuilder.setOmittedResourceCount(precount - resources.size()); 553 theResponsePageBuilder.setResources(resources); 554 theResponsePageBuilder.setIncludedResourceCount(includedPidList.size()); 555 556 return resources; 557 } 558 559 public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) { 560 myInterceptorBroadcaster = theInterceptorBroadcaster; 561 } 562 563 @VisibleForTesting 564 public void setSearchCacheSvcForUnitTest(ISearchCacheSvc theSearchCacheSvc) { 565 mySearchCacheSvc = theSearchCacheSvc; 566 } 567 568 @VisibleForTesting 569 public void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) { 570 myDaoRegistry = theDaoRegistry; 571 } 572 573 @VisibleForTesting 574 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 575 myStorageSettings = theStorageSettings; 576 } 577 578 @VisibleForTesting 579 public void setSearchBuilderFactoryForUnitTest(SearchBuilderFactory theSearchBuilderFactory) { 580 mySearchBuilderFactory = theSearchBuilderFactory; 581 } 582}