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