
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.dao.index; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 026import ca.uhn.fhir.jpa.api.model.PersistentIdToForcedIdMap; 027import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 028import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 029import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 030import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 031import ca.uhn.fhir.jpa.model.config.PartitionSettings; 032import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 033import ca.uhn.fhir.jpa.model.cross.JpaResourceLookup; 034import ca.uhn.fhir.jpa.model.dao.JpaPid; 035import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 036import ca.uhn.fhir.jpa.model.entity.ResourceTable; 037import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 038import ca.uhn.fhir.jpa.util.MemoryCacheService; 039import ca.uhn.fhir.jpa.util.QueryChunker; 040import ca.uhn.fhir.rest.api.server.storage.BaseResourcePersistentId; 041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 042import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 043import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 044import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 045import ca.uhn.fhir.util.TaskChunker; 046import com.google.common.annotations.VisibleForTesting; 047import com.google.common.collect.ListMultimap; 048import com.google.common.collect.MultimapBuilder; 049import jakarta.annotation.Nonnull; 050import jakarta.annotation.Nullable; 051import jakarta.persistence.EntityManager; 052import jakarta.persistence.PersistenceContext; 053import jakarta.persistence.PersistenceContextType; 054import jakarta.persistence.Tuple; 055import jakarta.persistence.TypedQuery; 056import jakarta.persistence.criteria.CriteriaBuilder; 057import jakarta.persistence.criteria.CriteriaQuery; 058import jakarta.persistence.criteria.Predicate; 059import jakarta.persistence.criteria.Root; 060import org.apache.commons.lang3.StringUtils; 061import org.apache.commons.lang3.Validate; 062import org.hl7.fhir.instance.model.api.IAnyResource; 063import org.hl7.fhir.instance.model.api.IBaseResource; 064import org.hl7.fhir.instance.model.api.IIdType; 065import org.slf4j.Logger; 066import org.slf4j.LoggerFactory; 067import org.springframework.beans.factory.annotation.Autowired; 068import org.springframework.stereotype.Service; 069import org.springframework.transaction.support.TransactionSynchronizationManager; 070 071import java.util.ArrayList; 072import java.util.Collection; 073import java.util.Date; 074import java.util.HashMap; 075import java.util.HashSet; 076import java.util.Iterator; 077import java.util.List; 078import java.util.Map; 079import java.util.Objects; 080import java.util.Optional; 081import java.util.Set; 082import java.util.stream.Collectors; 083 084import static ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder.replaceDefaultPartitionIdIfNonNull; 085import static ca.uhn.fhir.model.primitive.IdDt.isValidLong; 086import static org.apache.commons.lang3.StringUtils.isNotBlank; 087 088/** 089 * This class is used to convert between PIDs (the internal primary key for a particular resource as 090 * stored in the {@link ResourceTable HFJ_RESOURCE} table), and the 091 * public ID that a resource has. 092 * <p> 093 * These IDs are sometimes one and the same (by default, a resource that the server assigns the ID of 094 * <code>Patient/1</code> will simply use a PID of 1 and and ID of 1. However, they may also be different 095 * in cases where a forced ID is used (an arbitrary client-assigned ID). 096 * </p> 097 * <p> 098 * This service is highly optimized in order to minimize the number of DB calls as much as possible, 099 * since ID resolution is fundamental to many basic operations. This service returns either 100 * {@link IResourceLookup} or {@link BaseResourcePersistentId} depending on the method being called. 101 * The former involves an extra database join that the latter does not require, so selecting the 102 * right method here is important. 103 * </p> 104 */ 105@Service 106public class IdHelperService implements IIdHelperService<JpaPid> { 107 public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0]; 108 public static final String RESOURCE_PID = "RESOURCE_PID"; 109 private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class); 110 111 @Autowired 112 protected IResourceTableDao myResourceTableDao; 113 114 @Autowired 115 private JpaStorageSettings myStorageSettings; 116 117 @Autowired 118 private FhirContext myFhirCtx; 119 120 @Autowired 121 private MemoryCacheService myMemoryCacheService; 122 123 @Autowired 124 private IHapiTransactionService myTransactionService; 125 126 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 127 private EntityManager myEntityManager; 128 129 @Autowired 130 private PartitionSettings myPartitionSettings; 131 132 private boolean myDontCheckActiveTransactionForUnitTest; 133 134 @VisibleForTesting 135 protected void setDontCheckActiveTransactionForUnitTest(boolean theDontCheckActiveTransactionForUnitTest) { 136 myDontCheckActiveTransactionForUnitTest = theDontCheckActiveTransactionForUnitTest; 137 } 138 139 /** 140 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 141 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 142 * Optionally filters out deleted resources. 143 * 144 * @throws ResourceNotFoundException If the ID can not be found 145 */ 146 @Override 147 @Nonnull 148 public IResourceLookup<JpaPid> resolveResourceIdentity( 149 @Nonnull RequestPartitionId theRequestPartitionId, 150 @Nullable String theResourceType, 151 @Nonnull final String theResourceId, 152 @Nonnull ResolveIdentityMode theMode) 153 throws ResourceNotFoundException { 154 155 IIdType id; 156 boolean untyped; 157 if (theResourceType != null) { 158 untyped = false; 159 id = newIdType(theResourceType + "/" + theResourceId); 160 } else { 161 /* 162 * This shouldn't be common, but we need to be able to handle it. 163 * The only real known use case currently is when handing references 164 * in searches where the client didn't qualify the ID. E.g. 165 * /Provenance?target=A,B,C 166 * We emit a warning in this case that they should be qualfying the 167 * IDs, but we do stil allow it. 168 */ 169 untyped = true; 170 id = newIdType(theResourceId); 171 } 172 List<IIdType> ids = List.of(id); 173 Map<IIdType, IResourceLookup<JpaPid>> outcome = resolveResourceIdentities(theRequestPartitionId, ids, theMode); 174 175 // We only pass 1 input in so only 0..1 will come back 176 Validate.isTrue(outcome.size() <= 1, "Unexpected output size %s for ID: %s", outcome.size(), ids); 177 178 IResourceLookup<JpaPid> retVal; 179 if (untyped) { 180 if (outcome.isEmpty()) { 181 retVal = null; 182 } else { 183 retVal = outcome.values().iterator().next(); 184 } 185 } else { 186 retVal = outcome.get(id); 187 } 188 189 if (retVal == null) { 190 throw new ResourceNotFoundException(Msg.code(2001) + "Resource " + id + " is not known"); 191 } 192 193 return retVal; 194 } 195 196 @Nonnull 197 @Override 198 public Map<IIdType, IResourceLookup<JpaPid>> resolveResourceIdentities( 199 @Nonnull RequestPartitionId theRequestPartitionId, 200 Collection<IIdType> theIds, 201 ResolveIdentityMode theMode) { 202 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive() 203 : "no transaction active"; 204 205 if (theIds.isEmpty()) { 206 return new HashMap<>(); 207 } 208 209 Collection<IIdType> ids = new ArrayList<>(theIds); 210 Set<String> idsSet = new HashSet<>(ids.size()); 211 for (Iterator<IIdType> iterator = ids.iterator(); iterator.hasNext(); ) { 212 IIdType id = iterator.next(); 213 if (!id.hasIdPart()) { 214 throw new InvalidRequestException(Msg.code(1101) + "Parameter value missing in request"); 215 } 216 if (!idsSet.add(id.getValue())) { 217 iterator.remove(); 218 } 219 } 220 221 RequestPartitionId requestPartitionId = replaceDefault(theRequestPartitionId); 222 ListMultimap<IIdType, IResourceLookup<JpaPid>> idToLookup = 223 MultimapBuilder.hashKeys(ids.size()).arrayListValues(1).build(); 224 225 // Do we have any FHIR ID lookups cached for any of the IDs 226 if (theMode.isUseCache(myStorageSettings.isDeleteEnabled()) && !ids.isEmpty()) { 227 resolveResourceIdentitiesForFhirIdsUsingCache(requestPartitionId, theMode, ids, idToLookup); 228 } 229 230 // We still haven't found IDs, let's look them up in the DB 231 if (!ids.isEmpty()) { 232 myTransactionService 233 .withSystemRequest() 234 .withRequestPartitionId(requestPartitionId) 235 .execute(() -> 236 resolveResourceIdentitiesForFhirIdsUsingDatabase(requestPartitionId, ids, idToLookup)); 237 } 238 239 // Convert the multimap into a simple map 240 Map<IIdType, IResourceLookup<JpaPid>> retVal = new HashMap<>(idToLookup.size()); 241 for (Map.Entry<IIdType, IResourceLookup<JpaPid>> next : idToLookup.entries()) { 242 IResourceLookup<JpaPid> nextLookup = next.getValue(); 243 244 IIdType resourceId = myFhirCtx.getVersion().newIdType(nextLookup.getResourceType(), nextLookup.getFhirId()); 245 if (nextLookup.getDeleted() != null) { 246 if (theMode.isFailOnDeleted()) { 247 String msg = myFhirCtx 248 .getLocalizer() 249 .getMessageSanitized(IdHelperService.class, "deletedId", resourceId.getValue()); 250 throw new ResourceGoneException(Msg.code(2572) + msg); 251 } 252 if (!theMode.isIncludeDeleted()) { 253 continue; 254 } 255 } 256 257 nextLookup.getPersistentId().setAssociatedResourceId(resourceId); 258 259 IResourceLookup<JpaPid> previousValue = retVal.put(resourceId, nextLookup); 260 if (previousValue != null) { 261 /* 262 * This means that either: 263 * 1. There are two resources with the exact same resource type and forced 264 * id. The most likely reason for that is that someone is performing a 265 * multi-partition search and there are resources on each partition 266 * with the same ID. 267 * 2. The unique constraint on the FHIR_ID column has been dropped 268 */ 269 ourLog.warn("Resource ID[{}] corresponds to lookups: {} and {}", resourceId, previousValue, nextLookup); 270 String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); 271 throw new PreconditionFailedException(Msg.code(1099) + msg); 272 } 273 } 274 275 return retVal; 276 } 277 278 /** 279 * Fetch the resource identity ({@link IResourceLookup}) for a collection of 280 * resource IDs from the internal memory cache if possible. Note that we only 281 * use cached results if deletes are disabled on the server (since it is 282 * therefore not possible that we have an entry in the cache that has since 283 * been deleted but the cache doesn't know about the deletion), or if we 284 * aren't excluding deleted results anyhow. 285 * 286 * @param theRequestPartitionId The partition(s) to search 287 * @param theIdsToResolve The IDs we should look up. Any IDs that are resolved 288 * will be removed from this list. Any IDs remaining in 289 * the list after calling this method still haven't 290 * been attempted to be resolved. 291 * @param theMapToPopulate The results will be populated into this map 292 */ 293 private void resolveResourceIdentitiesForFhirIdsUsingCache( 294 @Nonnull RequestPartitionId theRequestPartitionId, 295 ResolveIdentityMode theMode, 296 Collection<IIdType> theIdsToResolve, 297 ListMultimap<IIdType, IResourceLookup<JpaPid>> theMapToPopulate) { 298 for (Iterator<IIdType> idIterator = theIdsToResolve.iterator(); idIterator.hasNext(); ) { 299 IIdType nextForcedId = idIterator.next(); 300 MemoryCacheService.ForcedIdCacheKey nextKey = new MemoryCacheService.ForcedIdCacheKey( 301 nextForcedId.getResourceType(), nextForcedId.getIdPart(), theRequestPartitionId); 302 if (theMode.isUseCache(myStorageSettings.isDeleteEnabled())) { 303 List<IResourceLookup<JpaPid>> cachedLookups = myMemoryCacheService.getIfPresent( 304 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, nextKey); 305 if (cachedLookups != null && !cachedLookups.isEmpty()) { 306 idIterator.remove(); 307 for (IResourceLookup<JpaPid> cachedLookup : cachedLookups) { 308 if (theMode.isIncludeDeleted() || cachedLookup.getDeleted() == null) { 309 theMapToPopulate.put(nextKey.toIdType(myFhirCtx), cachedLookup); 310 } 311 } 312 } 313 } 314 } 315 } 316 317 /** 318 * Fetch the resource identity ({@link IResourceLookup}) for a collection of 319 * resource IDs from the database 320 * 321 * @param theRequestPartitionId The partition(s) to search 322 * @param theIdsToResolve The IDs we should look up 323 * @param theMapToPopulate The results will be populated into this map 324 */ 325 private void resolveResourceIdentitiesForFhirIdsUsingDatabase( 326 RequestPartitionId theRequestPartitionId, 327 Collection<IIdType> theIdsToResolve, 328 ListMultimap<IIdType, IResourceLookup<JpaPid>> theMapToPopulate) { 329 330 /* 331 * If we have more than a threshold of IDs, we need to chunk the execution to 332 * avoid having too many parameters in one SQL statement 333 */ 334 int maxPageSize = (SearchBuilder.getMaximumPageSize() / 2) - 10; 335 if (theIdsToResolve.size() > maxPageSize) { 336 TaskChunker.chunk( 337 theIdsToResolve, 338 maxPageSize, 339 chunk -> resolveResourceIdentitiesForFhirIdsUsingDatabase( 340 theRequestPartitionId, chunk, theMapToPopulate)); 341 return; 342 } 343 344 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 345 CriteriaQuery<Tuple> criteriaQuery = cb.createTupleQuery(); 346 Root<ResourceTable> from = criteriaQuery.from(ResourceTable.class); 347 criteriaQuery.multiselect( 348 from.get("myPid"), 349 from.get("myResourceType"), 350 from.get("myFhirId"), 351 from.get("myDeleted"), 352 from.get("myPartitionIdValue")); 353 354 List<Predicate> outerAndPredicates = new ArrayList<>(2); 355 if (!theRequestPartitionId.isAllPartitions()) { 356 getOptionalPartitionPredicate(theRequestPartitionId, cb, from).ifPresent(outerAndPredicates::add); 357 } 358 359 // one create one clause per id. 360 List<Predicate> innerIdPredicates = new ArrayList<>(theIdsToResolve.size()); 361 for (IIdType next : theIdsToResolve) { 362 List<Predicate> idPredicates = new ArrayList<>(2); 363 364 if (isNotBlank(next.getResourceType())) { 365 Predicate typeCriteria = cb.equal(from.get("myResourceType"), next.getResourceType()); 366 idPredicates.add(typeCriteria); 367 } 368 Predicate idCriteria = cb.equal(from.get("myFhirId"), next.getIdPart()); 369 idPredicates.add(idCriteria); 370 371 innerIdPredicates.add(cb.and(idPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 372 } 373 outerAndPredicates.add(cb.or(innerIdPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 374 375 criteriaQuery.where(cb.and(outerAndPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 376 TypedQuery<Tuple> query = myEntityManager.createQuery(criteriaQuery); 377 List<Tuple> results = query.getResultList(); 378 for (Tuple nextId : results) { 379 // Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending. 380 JpaPid resourcePid = nextId.get(0, JpaPid.class); 381 String resourceType = nextId.get(1, String.class); 382 String fhirId = nextId.get(2, String.class); 383 Date deletedAd = nextId.get(3, Date.class); 384 Integer partitionId = nextId.get(4, Integer.class); 385 if (resourcePid != null) { 386 if (resourcePid.getPartitionId() == null && partitionId != null) { 387 resourcePid.setPartitionId(partitionId); 388 } 389 JpaResourceLookup lookup = new JpaResourceLookup( 390 resourceType, fhirId, resourcePid, deletedAd, PartitionablePartitionId.with(partitionId, null)); 391 392 MemoryCacheService.ForcedIdCacheKey nextKey = 393 new MemoryCacheService.ForcedIdCacheKey(resourceType, fhirId, theRequestPartitionId); 394 IIdType id = nextKey.toIdType(myFhirCtx); 395 theMapToPopulate.put(id, lookup); 396 397 List<IResourceLookup<JpaPid>> valueToCache = theMapToPopulate.get(id); 398 myMemoryCacheService.putAfterCommit( 399 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, nextKey, valueToCache); 400 } 401 } 402 } 403 404 /** 405 * Returns true if the given resource ID should be stored in a forced ID. Under default config 406 * (meaning client ID strategy is {@link JpaStorageSettings.ClientIdStrategyEnum#ALPHANUMERIC}) 407 * this will return true if the ID has any non-digit characters. 408 * <p> 409 * In {@link JpaStorageSettings.ClientIdStrategyEnum#ANY} mode it will always return true. 410 */ 411 @Override 412 public boolean idRequiresForcedId(String theId) { 413 return myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ANY 414 || !isValidPid(theId); 415 } 416 417 /** 418 * Return optional predicate for searching on forcedId 419 * 1. If the partition mode is ALLOWED_UNQUALIFIED, the return optional predicate will be empty, so search is across all partitions. 420 * 2. If it is default partition and default partition id is null, then return predicate for null partition. 421 * 3. If the requested partition search is not all partition, return the request partition as predicate. 422 */ 423 private Optional<Predicate> getOptionalPartitionPredicate( 424 RequestPartitionId theRequestPartitionId, CriteriaBuilder cb, Root<ResourceTable> from) { 425 if (myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()) { 426 return Optional.empty(); 427 } else if (theRequestPartitionId.isAllPartitions()) { 428 return Optional.empty(); 429 } else { 430 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds(); 431 partitionIds = replaceDefaultPartitionIdIfNonNull(myPartitionSettings, partitionIds); 432 if (partitionIds.contains(null)) { 433 Predicate partitionIdNullCriteria = 434 from.get("myPartitionIdValue").isNull(); 435 if (partitionIds.size() == 1) { 436 return Optional.of(partitionIdNullCriteria); 437 } else { 438 Predicate partitionIdCriteria = from.get("myPartitionIdValue") 439 .in(partitionIds.stream().filter(Objects::nonNull).collect(Collectors.toList())); 440 return Optional.of(cb.or(partitionIdCriteria, partitionIdNullCriteria)); 441 } 442 } else { 443 if (partitionIds.size() > 1) { 444 Predicate partitionIdCriteria = 445 from.get("myPartitionIdValue").in(partitionIds); 446 return Optional.of(partitionIdCriteria); 447 } else if (partitionIds.size() == 1) { 448 Predicate partitionIdCriteria = cb.equal(from.get("myPartitionIdValue"), partitionIds.get(0)); 449 return Optional.of(partitionIdCriteria); 450 } 451 } 452 } 453 return Optional.empty(); 454 } 455 456 private void populateAssociatedResourceId(String nextResourceType, String forcedId, JpaPid jpaPid) { 457 IIdType resourceId = myFhirCtx.getVersion().newIdType(); 458 resourceId.setValue(nextResourceType + "/" + forcedId); 459 jpaPid.setAssociatedResourceId(resourceId); 460 } 461 462 /** 463 * Given a persistent ID, returns the associated resource ID 464 */ 465 @Nonnull 466 @Override 467 public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, JpaPid theId) { 468 if (theId.getAssociatedResourceId() != null) { 469 return theId.getAssociatedResourceId(); 470 } 471 472 IIdType retVal = theCtx.getVersion().newIdType(); 473 474 Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId); 475 if (forcedId.isPresent()) { 476 retVal.setValue(forcedId.get()); 477 } else { 478 retVal.setValue(theResourceType + '/' + theId.getId()); 479 } 480 481 return retVal; 482 } 483 484 @SuppressWarnings("OptionalAssignedToNull") 485 @Override 486 public Optional<String> translatePidIdToForcedIdWithCache(JpaPid theId) { 487 // do getIfPresent and then put to avoid doing I/O inside the cache. 488 Optional<String> forcedId = 489 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId); 490 491 if (forcedId == null) { 492 // This is only called when we know the resource exists. 493 // So this optional is only empty when there is no hfj_forced_id table 494 // note: this is obsolete with the new fhir_id column, and will go away. 495 forcedId = myTransactionService 496 .withSystemRequest() 497 .withRequestPartitionId(RequestPartitionId.fromPartitionIds(theId.getPartitionId())) 498 .execute(() -> myResourceTableDao.findById(theId).map(ResourceTable::asTypedFhirResourceId)); 499 500 myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId, forcedId); 501 } 502 503 return forcedId; 504 } 505 506 public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) { 507 if (myPartitionSettings.getDefaultPartitionId() != null) { 508 if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) { 509 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream() 510 .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t) 511 .collect(Collectors.toList()); 512 return RequestPartitionId.fromPartitionIds(partitionIds); 513 } 514 } 515 return theRequestPartitionId; 516 } 517 518 @Override 519 public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) { 520 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 521 HashMap<JpaPid, Optional<String>> retVal = new HashMap<>( 522 myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourceIds)); 523 524 List<JpaPid> remainingPids = 525 theResourceIds.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 526 527 QueryChunker.chunk(remainingPids, t -> { 528 List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t); 529 530 for (ResourceTable nextResourceEntity : resourceEntities) { 531 JpaPid nextResourcePid = nextResourceEntity.getPersistentId(); 532 Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId()); 533 retVal.put(nextResourcePid, nextForcedId); 534 myMemoryCacheService.putAfterCommit( 535 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId); 536 } 537 }); 538 539 remainingPids = 540 theResourceIds.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 541 for (JpaPid nextResourcePid : remainingPids) { 542 retVal.put(nextResourcePid, Optional.empty()); 543 myMemoryCacheService.putAfterCommit( 544 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty()); 545 } 546 Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>(retVal); 547 548 return new PersistentIdToForcedIdMap<>(convertRetVal); 549 } 550 551 /** 552 * This method can be called to pre-emptively add entries to the ID cache. It should 553 * be called by DAO methods if they are creating or changing the deleted status 554 * of a resource. This method returns immediately, but the data is not 555 * added to the internal caches until the current DB transaction is successfully 556 * committed, and nothing is added if the transaction rolls back. 557 */ 558 @Override 559 public void addResolvedPidToFhirIdAfterCommit( 560 @Nonnull JpaPid theJpaPid, 561 @Nonnull RequestPartitionId theRequestPartitionId, 562 @Nonnull String theResourceType, 563 @Nonnull String theFhirId, 564 @Nullable Date theDeletedAt) { 565 if (theJpaPid.getAssociatedResourceId() == null) { 566 populateAssociatedResourceId(theResourceType, theFhirId, theJpaPid); 567 } 568 569 myMemoryCacheService.putAfterCommit( 570 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, 571 theJpaPid, 572 Optional.of(theResourceType + "/" + theFhirId)); 573 574 JpaResourceLookup lookup = new JpaResourceLookup( 575 theResourceType, theFhirId, theJpaPid.getId(), theDeletedAt, theJpaPid.getPartitionablePartitionId()); 576 577 MemoryCacheService.ForcedIdCacheKey fhirIdKey = 578 new MemoryCacheService.ForcedIdCacheKey(theResourceType, theFhirId, theRequestPartitionId); 579 myMemoryCacheService.putAfterCommit( 580 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKey, List.of(lookup)); 581 582 // If it's a pure-numeric ID, store it in the cache without a type as well 583 // so that we can resolve it this way when loading entities for update 584 if (myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC 585 && isValidLong(theFhirId)) { 586 MemoryCacheService.ForcedIdCacheKey fhirIdKeyWithoutType = 587 new MemoryCacheService.ForcedIdCacheKey(null, theFhirId, theRequestPartitionId); 588 myMemoryCacheService.putAfterCommit( 589 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKeyWithoutType, List.of(lookup)); 590 } 591 } 592 593 @VisibleForTesting 594 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 595 myPartitionSettings = thePartitionSettings; 596 } 597 598 @Override 599 @Nullable 600 public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) { 601 Object resourceId = theResource.getUserData(RESOURCE_PID); 602 JpaPid retVal; 603 if (resourceId == null) { 604 IIdType id = theResource.getIdElement(); 605 try { 606 retVal = resolveResourceIdentityPid( 607 theRequestPartitionId, 608 id.getResourceType(), 609 id.getIdPart(), 610 ResolveIdentityMode.includeDeleted().cacheOk()); 611 } catch (ResourceNotFoundException e) { 612 retVal = null; 613 } 614 } else { 615 retVal = (JpaPid) resourceId; 616 } 617 return retVal; 618 } 619 620 @Override 621 @Nonnull 622 public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) { 623 JpaPid theResourcePID = (JpaPid) theResource.getUserData(RESOURCE_PID); 624 if (theResourcePID == null) { 625 throw new IllegalStateException(Msg.code(2108) 626 + String.format( 627 "Unable to find %s in the user data for %s with ID %s", 628 RESOURCE_PID, theResource, theResource.getId())); 629 } 630 return theResourcePID; 631 } 632 633 @Override 634 public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) { 635 Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid); 636 if (optionalResource.isEmpty()) { 637 throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found"); 638 } 639 return optionalResource.get().getIdDt().toVersionless(); 640 } 641 642 /** 643 * Given a set of PIDs, return a set of public FHIR Resource IDs. 644 * This function will resolve a forced ID if it resolves, and if it fails to resolve to a forced it, will just return the pid 645 * Example: 646 * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows: 647 * <p> 648 * [1,2,3] -> ["1","pat1","3"] 649 * 650 * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs. 651 * @return A Set of strings representing the FHIR IDs of the pids. 652 */ 653 @Override 654 public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) { 655 assert TransactionSynchronizationManager.isSynchronizationActive(); 656 657 PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids); 658 659 return pidToForcedIdMap.getResolvedResourceIds(); 660 } 661 662 @Override 663 public JpaPid newPid(Object thePid) { 664 return JpaPid.fromId((Long) thePid); 665 } 666 667 @Override 668 public JpaPid newPid(Object thePid, Integer thePartitionId) { 669 return JpaPid.fromId((Long) thePid, thePartitionId); 670 } 671 672 @Override 673 public JpaPid newPidFromStringIdAndResourceName(Integer thePartitionId, String thePid, String theResourceName) { 674 JpaPid retVal = JpaPid.fromId(Long.parseLong(thePid), thePartitionId); 675 retVal.setResourceType(theResourceName); 676 return retVal; 677 } 678 679 private IIdType newIdType(String theValue) { 680 IIdType retVal = myFhirCtx.getVersion().newIdType(); 681 retVal.setValue(theValue); 682 return retVal; 683 } 684 685 public static boolean isValidPid(String theIdPart) { 686 return StringUtils.isNumeric(theIdPart); 687 } 688}