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