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