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.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.hl7.fhir.r4.model.IdType; 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.Collections; 074import java.util.Date; 075import java.util.HashMap; 076import java.util.HashSet; 077import java.util.Iterator; 078import java.util.List; 079import java.util.Map; 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 ca.uhn.fhir.jpa.model.entity.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 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 124 private EntityManager myEntityManager; 125 126 @Autowired 127 private PartitionSettings myPartitionSettings; 128 129 private boolean myDontCheckActiveTransactionForUnitTest; 130 131 @VisibleForTesting 132 protected void setDontCheckActiveTransactionForUnitTest(boolean theDontCheckActiveTransactionForUnitTest) { 133 myDontCheckActiveTransactionForUnitTest = theDontCheckActiveTransactionForUnitTest; 134 } 135 136 /** 137 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 138 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 139 * Optionally filters out deleted resources. 140 * 141 * @throws ResourceNotFoundException If the ID can not be found 142 */ 143 @Override 144 @Nonnull 145 public IResourceLookup<JpaPid> resolveResourceIdentity( 146 @Nonnull RequestPartitionId theRequestPartitionId, 147 @Nullable String theResourceType, 148 @Nonnull final String theResourceId, 149 @Nonnull ResolveIdentityMode theMode) 150 throws ResourceNotFoundException { 151 152 IIdType id; 153 if (theResourceType != null) { 154 id = newIdType(theResourceType + "/" + theResourceId); 155 } else { 156 id = newIdType(theResourceId); 157 } 158 List<IIdType> ids = List.of(id); 159 Map<IIdType, IResourceLookup<JpaPid>> outcome = resolveResourceIdentities(theRequestPartitionId, ids, theMode); 160 161 // We only pass 1 input in so only 0..1 will come back 162 if (!outcome.containsKey(id)) { 163 throw new ResourceNotFoundException(Msg.code(2001) + "Resource " + id + " is not known"); 164 } 165 166 return outcome.get(id); 167 } 168 169 @Nonnull 170 @Override 171 public Map<IIdType, IResourceLookup<JpaPid>> resolveResourceIdentities( 172 @Nonnull RequestPartitionId theRequestPartitionId, 173 Collection<IIdType> theIds, 174 ResolveIdentityMode theMode) { 175 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive() 176 : "no transaction active"; 177 178 if (theIds.isEmpty()) { 179 return new HashMap<>(); 180 } 181 182 Collection<IIdType> ids = new ArrayList<>(theIds); 183 ids.forEach(id -> Validate.isTrue(id.hasIdPart())); 184 185 RequestPartitionId requestPartitionId = replaceDefault(theRequestPartitionId); 186 ListMultimap<IIdType, IResourceLookup<JpaPid>> idToLookup = 187 MultimapBuilder.hashKeys(theIds.size()).arrayListValues(1).build(); 188 189 // Do we have any FHIR ID lookups cached for any of the IDs 190 if (theMode.isUseCache(myStorageSettings.isDeleteEnabled()) && !ids.isEmpty()) { 191 resolveResourceIdentitiesForFhirIdsUsingCache(requestPartitionId, theMode, ids, idToLookup); 192 } 193 194 // We still haven't found IDs, let's look them up in the DB 195 if (!ids.isEmpty()) { 196 resolveResourceIdentitiesForFhirIdsUsingDatabase(requestPartitionId, ids, idToLookup); 197 } 198 199 // Convert the multimap into a simple map 200 Map<IIdType, IResourceLookup<JpaPid>> retVal = new HashMap<>(); 201 for (Map.Entry<IIdType, IResourceLookup<JpaPid>> next : idToLookup.entries()) { 202 if (next.getValue().getDeleted() != null) { 203 if (theMode.isFailOnDeleted()) { 204 String msg = myFhirCtx 205 .getLocalizer() 206 .getMessageSanitized( 207 IdHelperService.class, 208 "deletedId", 209 next.getKey().getValue()); 210 throw new ResourceGoneException(Msg.code(2572) + msg); 211 } 212 if (!theMode.isIncludeDeleted()) { 213 continue; 214 } 215 } 216 217 IResourceLookup previousValue = retVal.put(next.getKey(), next.getValue()); 218 if (previousValue != null) { 219 /* 220 * This means that either: 221 * 1. There are two resources with the exact same resource type and forced 222 * id. The most likely reason for that is that someone is performing a 223 * multi-partition search and there are resources on each partition 224 * with the same ID. 225 * 2. The unique constraint on the FHIR_ID column has been dropped 226 */ 227 ourLog.warn( 228 "Resource ID[{}] corresponds to lookups: {} and {}", 229 next.getKey(), 230 previousValue, 231 next.getValue()); 232 String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); 233 throw new PreconditionFailedException(Msg.code(1099) + msg); 234 } 235 } 236 237 return retVal; 238 } 239 240 /** 241 * Fetch the resource identity ({@link IResourceLookup}) for a collection of 242 * resource IDs from the internal memory cache if possible. Note that we only 243 * use cached results if deletes are disabled on the server (since it is 244 * therefore not possible that we have an entry in the cache that has since 245 * been deleted but the cache doesn't know about the deletion), or if we 246 * aren't excluding deleted results anyhow. 247 * 248 * @param theRequestPartitionId The partition(s) to search 249 * @param theIdsToResolve The IDs we should look up. Any IDs that are resolved 250 * will be removed from this list. Any IDs remaining in 251 * the list after calling this method still haven't 252 * been attempted to be resolved. 253 * @param theMapToPopulate The results will be populated into this map 254 */ 255 private void resolveResourceIdentitiesForFhirIdsUsingCache( 256 @Nonnull RequestPartitionId theRequestPartitionId, 257 ResolveIdentityMode theMode, 258 Collection<IIdType> theIdsToResolve, 259 ListMultimap<IIdType, IResourceLookup<JpaPid>> theMapToPopulate) { 260 for (Iterator<IIdType> idIterator = theIdsToResolve.iterator(); idIterator.hasNext(); ) { 261 IIdType nextForcedId = idIterator.next(); 262 MemoryCacheService.ForcedIdCacheKey nextKey = new MemoryCacheService.ForcedIdCacheKey( 263 nextForcedId.getResourceType(), nextForcedId.getIdPart(), theRequestPartitionId); 264 if (theMode.isUseCache(myStorageSettings.isDeleteEnabled())) { 265 List<IResourceLookup<JpaPid>> cachedLookups = myMemoryCacheService.getIfPresent( 266 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, nextKey); 267 if (cachedLookups != null && !cachedLookups.isEmpty()) { 268 idIterator.remove(); 269 for (IResourceLookup<JpaPid> cachedLookup : cachedLookups) { 270 if (theMode.isIncludeDeleted() || cachedLookup.getDeleted() == null) { 271 theMapToPopulate.put(nextKey.toIdType(myFhirCtx), cachedLookup); 272 } 273 } 274 } 275 } 276 } 277 } 278 279 /** 280 * Fetch the resource identity ({@link IResourceLookup}) for a collection of 281 * resource IDs from the database 282 * 283 * @param theRequestPartitionId The partition(s) to search 284 * @param theIdsToResolve The IDs we should look up 285 * @param theMapToPopulate The results will be populated into this map 286 */ 287 private void resolveResourceIdentitiesForFhirIdsUsingDatabase( 288 RequestPartitionId theRequestPartitionId, 289 Collection<IIdType> theIdsToResolve, 290 ListMultimap<IIdType, IResourceLookup<JpaPid>> theMapToPopulate) { 291 292 /* 293 * If we have more than a threshold of IDs, we need to chunk the execution to 294 * avoid having too many parameters in one SQL statement 295 */ 296 int maxPageSize = (SearchBuilder.getMaximumPageSize() / 2) - 10; 297 if (theIdsToResolve.size() > maxPageSize) { 298 TaskChunker.chunk( 299 theIdsToResolve, 300 maxPageSize, 301 chunk -> resolveResourceIdentitiesForFhirIdsUsingDatabase( 302 theRequestPartitionId, chunk, theMapToPopulate)); 303 return; 304 } 305 306 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 307 CriteriaQuery<Tuple> criteriaQuery = cb.createTupleQuery(); 308 Root<ResourceTable> from = criteriaQuery.from(ResourceTable.class); 309 criteriaQuery.multiselect( 310 from.get("myId"), 311 from.get("myResourceType"), 312 from.get("myFhirId"), 313 from.get("myDeleted"), 314 from.get("myPartitionIdValue")); 315 316 List<Predicate> outerAndPredicates = new ArrayList<>(2); 317 if (!theRequestPartitionId.isAllPartitions()) { 318 getOptionalPartitionPredicate(theRequestPartitionId, cb, from).ifPresent(outerAndPredicates::add); 319 } 320 321 // one create one clause per id. 322 List<Predicate> innerIdPredicates = new ArrayList<>(theIdsToResolve.size()); 323 boolean haveUntypedIds = false; 324 for (IIdType next : theIdsToResolve) { 325 if (!next.hasResourceType()) { 326 haveUntypedIds = true; 327 } 328 329 List<Predicate> idPredicates = new ArrayList<>(2); 330 331 if (myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC 332 && next.isIdPartValidLong()) { 333 Predicate typeCriteria = cb.equal(from.get("myId"), next.getIdPartAsLong()); 334 idPredicates.add(typeCriteria); 335 } else { 336 if (isNotBlank(next.getResourceType())) { 337 Predicate typeCriteria = cb.equal(from.get("myResourceType"), next.getResourceType()); 338 idPredicates.add(typeCriteria); 339 } 340 Predicate idCriteria = cb.equal(from.get("myFhirId"), next.getIdPart()); 341 idPredicates.add(idCriteria); 342 } 343 344 innerIdPredicates.add(cb.and(idPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 345 } 346 outerAndPredicates.add(cb.or(innerIdPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 347 348 criteriaQuery.where(cb.and(outerAndPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 349 TypedQuery<Tuple> query = myEntityManager.createQuery(criteriaQuery); 350 List<Tuple> results = query.getResultList(); 351 for (Tuple nextId : results) { 352 // Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending. 353 Long resourcePid = nextId.get(0, Long.class); 354 String resourceType = nextId.get(1, String.class); 355 String fhirId = nextId.get(2, String.class); 356 Date deletedAd = nextId.get(3, Date.class); 357 Integer partitionId = nextId.get(4, Integer.class); 358 if (resourcePid != null) { 359 JpaResourceLookup lookup = new JpaResourceLookup( 360 resourceType, resourcePid, deletedAd, PartitionablePartitionId.with(partitionId, null)); 361 362 MemoryCacheService.ForcedIdCacheKey nextKey = 363 new MemoryCacheService.ForcedIdCacheKey(resourceType, fhirId, theRequestPartitionId); 364 IIdType id = nextKey.toIdType(myFhirCtx); 365 theMapToPopulate.put(id, lookup); 366 367 if (haveUntypedIds) { 368 id = nextKey.toIdTypeWithoutResourceType(myFhirCtx); 369 theMapToPopulate.put(id, lookup); 370 } 371 372 List<IResourceLookup<JpaPid>> valueToCache = theMapToPopulate.get(id); 373 myMemoryCacheService.putAfterCommit( 374 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, nextKey, valueToCache); 375 } 376 } 377 } 378 379 /** 380 * Returns a mapping of Id -> IResourcePersistentId. 381 * If any resource is not found, it will throw ResourceNotFound exception (and no map will be returned) 382 * Optionally filters out deleted resources. 383 */ 384 @Override 385 @Nonnull 386 public Map<String, JpaPid> resolveResourcePersistentIds( 387 @Nonnull RequestPartitionId theRequestPartitionId, 388 String theResourceType, 389 List<String> theIds, 390 ResolveIdentityMode theMode) { 391 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 392 Validate.notNull(theIds, "theIds cannot be null"); 393 Validate.isTrue(!theIds.isEmpty(), "theIds must not be empty"); 394 395 Map<String, JpaPid> retVals = new HashMap<>(); 396 for (String id : theIds) { 397 JpaPid retVal; 398 if (!idRequiresForcedId(id)) { 399 // is already a PID 400 retVal = JpaPid.fromId(Long.parseLong(id)); 401 retVals.put(id, retVal); 402 } else { 403 // is a forced id 404 // we must resolve! 405 if (myStorageSettings.isDeleteEnabled()) { 406 retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, id, theMode) 407 .getPersistentId(); 408 retVals.put(id, retVal); 409 } else { 410 // fetch from cache... adding to cache if not available 411 String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, id); 412 retVal = myMemoryCacheService.getThenPutAfterCommit( 413 MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> { 414 List<IIdType> ids = Collections.singletonList(new IdType(theResourceType, id)); 415 // fetches from cache using a function that checks cache first... 416 List<JpaPid> resolvedIds = 417 resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); 418 if (resolvedIds.isEmpty()) { 419 throw new ResourceNotFoundException(Msg.code(1100) + ids.get(0)); 420 } 421 return resolvedIds.get(0); 422 }); 423 retVals.put(id, retVal); 424 } 425 } 426 } 427 428 return retVals; 429 } 430 431 /** 432 * Given a resource type and ID, determines the internal persistent ID for the resource. 433 * Optionally filters out deleted resources. 434 * 435 * @throws ResourceNotFoundException If the ID can not be found 436 */ 437 @Nonnull 438 @Override 439 public JpaPid resolveResourcePersistentIds( 440 @Nonnull RequestPartitionId theRequestPartitionId, 441 String theResourceType, 442 String theId, 443 ResolveIdentityMode theMode) { 444 Validate.notNull(theId, "theId must not be null"); 445 446 Map<String, JpaPid> retVal = resolveResourcePersistentIds( 447 theRequestPartitionId, theResourceType, Collections.singletonList(theId), theMode); 448 return retVal.get(theId); // should be only one 449 } 450 451 /** 452 * Returns true if the given resource ID should be stored in a forced ID. Under default config 453 * (meaning client ID strategy is {@link JpaStorageSettings.ClientIdStrategyEnum#ALPHANUMERIC}) 454 * this will return true if the ID has any non-digit characters. 455 * <p> 456 * In {@link JpaStorageSettings.ClientIdStrategyEnum#ANY} mode it will always return true. 457 */ 458 @Override 459 public boolean idRequiresForcedId(String theId) { 460 return myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ANY 461 || !isValidPid(theId); 462 } 463 464 @Nonnull 465 private String toForcedIdToPidKey( 466 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theId) { 467 return RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + theResourceType + "/" + theId; 468 } 469 470 /** 471 * Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs. 472 * <p> 473 * This implementation will always try to use a cache for performance, meaning that it can resolve resources that 474 * are deleted (but note that forced IDs can't change, so the cache can't return incorrect results) 475 */ 476 @Override 477 @Nonnull 478 public List<JpaPid> resolveResourcePersistentIdsWithCache( 479 RequestPartitionId theRequestPartitionId, List<IIdType> theIds) { 480 boolean onlyForcedIds = false; 481 return resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds, onlyForcedIds); 482 } 483 484 /** 485 * Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs. 486 * <p> 487 * This implementation will always try to use a cache for performance, meaning that it can resolve resources that 488 * are deleted (but note that forced IDs can't change, so the cache can't return incorrect results) 489 * 490 * @param theOnlyForcedIds If <code>true</code>, resources which are not existing forced IDs will not be resolved 491 */ 492 @Override 493 @Nonnull 494 public List<JpaPid> resolveResourcePersistentIdsWithCache( 495 @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds, boolean theOnlyForcedIds) { 496 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 497 498 List<JpaPid> retVal = new ArrayList<>(theIds.size()); 499 500 for (IIdType id : theIds) { 501 if (!id.hasIdPart()) { 502 throw new InvalidRequestException(Msg.code(1101) + "Parameter value missing in request"); 503 } 504 } 505 506 if (!theIds.isEmpty()) { 507 Set<IIdType> idsToCheck = new HashSet<>(theIds.size()); 508 for (IIdType nextId : theIds) { 509 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY) { 510 if (nextId.isIdPartValidLong()) { 511 if (!theOnlyForcedIds) { 512 JpaPid jpaPid = JpaPid.fromId(nextId.getIdPartAsLong()); 513 jpaPid.setAssociatedResourceId(nextId); 514 retVal.add(jpaPid); 515 } 516 continue; 517 } 518 } 519 520 String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getIdPart()); 521 JpaPid cachedId = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key); 522 if (cachedId != null) { 523 retVal.add(cachedId); 524 continue; 525 } 526 527 idsToCheck.add(nextId); 528 } 529 new QueryChunker<IIdType>(); 530 TaskChunker.chunk( 531 idsToCheck, 532 SearchBuilder.getMaximumPageSize() / 2, 533 ids -> doResolvePersistentIds(theRequestPartitionId, ids, retVal)); 534 } 535 536 return retVal; 537 } 538 539 private void doResolvePersistentIds( 540 RequestPartitionId theRequestPartitionId, List<IIdType> theIds, List<JpaPid> theOutputListToPopulate) { 541 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 542 CriteriaQuery<Tuple> criteriaQuery = cb.createTupleQuery(); 543 Root<ResourceTable> from = criteriaQuery.from(ResourceTable.class); 544 545 /* 546 * IDX_RES_FHIR_ID covers these columns, but RES_ID is only INCLUDEd. 547 * Only PG, and MSSql support INCLUDE COLUMNS. 548 * @see AddIndexTask.generateSql 549 */ 550 criteriaQuery.multiselect(from.get("myId"), from.get("myResourceType"), from.get("myFhirId")); 551 552 // one create one clause per id. 553 List<Predicate> predicates = new ArrayList<>(theIds.size()); 554 for (IIdType next : theIds) { 555 556 List<Predicate> andPredicates = new ArrayList<>(3); 557 558 if (isNotBlank(next.getResourceType())) { 559 Predicate typeCriteria = cb.equal(from.get("myResourceType"), next.getResourceType()); 560 andPredicates.add(typeCriteria); 561 } 562 563 Predicate idCriteria = cb.equal(from.get("myFhirId"), next.getIdPart()); 564 andPredicates.add(idCriteria); 565 getOptionalPartitionPredicate(theRequestPartitionId, cb, from).ifPresent(andPredicates::add); 566 predicates.add(cb.and(andPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 567 } 568 569 // join all the clauses as OR 570 criteriaQuery.where(cb.or(predicates.toArray(EMPTY_PREDICATE_ARRAY))); 571 572 TypedQuery<Tuple> query = myEntityManager.createQuery(criteriaQuery); 573 List<Tuple> results = query.getResultList(); 574 for (Tuple nextId : results) { 575 // Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending. 576 Long resourceId = nextId.get(0, Long.class); 577 String resourceType = nextId.get(1, String.class); 578 String forcedId = nextId.get(2, String.class); 579 if (resourceId != null) { 580 JpaPid jpaPid = JpaPid.fromId(resourceId); 581 populateAssociatedResourceId(resourceType, forcedId, jpaPid); 582 theOutputListToPopulate.add(jpaPid); 583 584 String key = toForcedIdToPidKey(theRequestPartitionId, resourceType, forcedId); 585 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, jpaPid); 586 } 587 } 588 } 589 590 /** 591 * Return optional predicate for searching on forcedId 592 * 1. If the partition mode is ALLOWED_UNQUALIFIED, the return optional predicate will be empty, so search is across all partitions. 593 * 2. If it is default partition and default partition id is null, then return predicate for null partition. 594 * 3. If the requested partition search is not all partition, return the request partition as predicate. 595 */ 596 private Optional<Predicate> getOptionalPartitionPredicate( 597 RequestPartitionId theRequestPartitionId, CriteriaBuilder cb, Root<ResourceTable> from) { 598 if (myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()) { 599 return Optional.empty(); 600 } else if (theRequestPartitionId.isAllPartitions()) { 601 return Optional.empty(); 602 } else { 603 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds(); 604 partitionIds = replaceDefaultPartitionIdIfNonNull(myPartitionSettings, partitionIds); 605 if (partitionIds.contains(null)) { 606 Predicate partitionIdNullCriteria = 607 from.get("myPartitionIdValue").isNull(); 608 if (partitionIds.size() == 1) { 609 return Optional.of(partitionIdNullCriteria); 610 } else { 611 Predicate partitionIdCriteria = from.get("myPartitionIdValue") 612 .in(partitionIds.stream().filter(t -> t != null).collect(Collectors.toList())); 613 return Optional.of(cb.or(partitionIdCriteria, partitionIdNullCriteria)); 614 } 615 } else { 616 if (partitionIds.size() > 1) { 617 Predicate partitionIdCriteria = 618 from.get("myPartitionIdValue").in(partitionIds); 619 return Optional.of(partitionIdCriteria); 620 } else if (partitionIds.size() == 1) { 621 Predicate partitionIdCriteria = cb.equal(from.get("myPartitionIdValue"), partitionIds.get(0)); 622 return Optional.of(partitionIdCriteria); 623 } 624 } 625 } 626 return Optional.empty(); 627 } 628 629 private void populateAssociatedResourceId(String nextResourceType, String forcedId, JpaPid jpaPid) { 630 IIdType resourceId = myFhirCtx.getVersion().newIdType(); 631 resourceId.setValue(nextResourceType + "/" + forcedId); 632 jpaPid.setAssociatedResourceId(resourceId); 633 } 634 635 /** 636 * Given a persistent ID, returns the associated resource ID 637 */ 638 @Nonnull 639 @Override 640 public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, JpaPid theId) { 641 if (theId.getAssociatedResourceId() != null) { 642 return theId.getAssociatedResourceId(); 643 } 644 645 IIdType retVal = theCtx.getVersion().newIdType(); 646 647 Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId); 648 if (forcedId.isPresent()) { 649 retVal.setValue(forcedId.get()); 650 } else { 651 retVal.setValue(theResourceType + '/' + theId); 652 } 653 654 return retVal; 655 } 656 657 @SuppressWarnings("OptionalAssignedToNull") 658 @Override 659 public Optional<String> translatePidIdToForcedIdWithCache(JpaPid theId) { 660 // do getIfPresent and then put to avoid doing I/O inside the cache. 661 Optional<String> forcedId = 662 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId()); 663 664 if (forcedId == null) { 665 // This is only called when we know the resource exists. 666 // So this optional is only empty when there is no hfj_forced_id table 667 // note: this is obsolete with the new fhir_id column, and will go away. 668 forcedId = myResourceTableDao.findById(theId.getId()).map(ResourceTable::asTypedFhirResourceId); 669 myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId(), forcedId); 670 } 671 672 return forcedId; 673 } 674 675 public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) { 676 if (myPartitionSettings.getDefaultPartitionId() != null) { 677 if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) { 678 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream() 679 .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t) 680 .collect(Collectors.toList()); 681 return RequestPartitionId.fromPartitionIds(partitionIds); 682 } 683 } 684 return theRequestPartitionId; 685 } 686 687 @Override 688 public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) { 689 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 690 Set<Long> thePids = theResourceIds.stream().map(JpaPid::getId).collect(Collectors.toSet()); 691 Map<Long, Optional<String>> retVal = new HashMap<>( 692 myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, thePids)); 693 694 List<Long> remainingPids = 695 thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 696 697 QueryChunker.chunk(remainingPids, t -> { 698 List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t); 699 700 for (ResourceTable nextResourceEntity : resourceEntities) { 701 Long nextResourcePid = nextResourceEntity.getId(); 702 Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId()); 703 retVal.put(nextResourcePid, nextForcedId); 704 myMemoryCacheService.putAfterCommit( 705 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId); 706 } 707 }); 708 709 remainingPids = thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 710 for (Long nextResourcePid : remainingPids) { 711 retVal.put(nextResourcePid, Optional.empty()); 712 myMemoryCacheService.putAfterCommit( 713 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty()); 714 } 715 Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>(); 716 retVal.forEach((k, v) -> convertRetVal.put(JpaPid.fromId(k), v)); 717 718 return new PersistentIdToForcedIdMap<>(convertRetVal); 719 } 720 721 /** 722 * Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods 723 */ 724 @Override 725 public void addResolvedPidToFhirId( 726 @Nonnull JpaPid theJpaPid, 727 @Nonnull RequestPartitionId theRequestPartitionId, 728 @Nonnull String theResourceType, 729 @Nonnull String theFhirId, 730 @Nullable Date theDeletedAt) { 731 if (theJpaPid.getAssociatedResourceId() == null) { 732 populateAssociatedResourceId(theResourceType, theFhirId, theJpaPid); 733 } 734 735 myMemoryCacheService.putAfterCommit( 736 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, 737 theJpaPid.getId(), 738 Optional.of(theResourceType + "/" + theFhirId)); 739 String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theFhirId); 740 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theJpaPid); 741 742 JpaResourceLookup lookup = new JpaResourceLookup( 743 theResourceType, theJpaPid.getId(), theDeletedAt, theJpaPid.getPartitionablePartitionId()); 744 745 MemoryCacheService.ForcedIdCacheKey fhirIdKey = 746 new MemoryCacheService.ForcedIdCacheKey(theResourceType, theFhirId, theRequestPartitionId); 747 myMemoryCacheService.putAfterCommit( 748 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKey, List.of(lookup)); 749 750 // If it's a pure-numeric ID, store it in the cache without a type as well 751 // so that we can resolve it this way when loading entities for update 752 if (myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC 753 && isValidLong(theFhirId)) { 754 MemoryCacheService.ForcedIdCacheKey fhirIdKeyWithoutType = 755 new MemoryCacheService.ForcedIdCacheKey(null, theFhirId, theRequestPartitionId); 756 myMemoryCacheService.putAfterCommit( 757 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKeyWithoutType, List.of(lookup)); 758 } 759 } 760 761 @VisibleForTesting 762 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 763 myPartitionSettings = thePartitionSettings; 764 } 765 766 @Override 767 @Nonnull 768 public List<JpaPid> getPidsOrThrowException( 769 @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds) { 770 return resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds); 771 } 772 773 @Override 774 @Nullable 775 public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) { 776 Object resourceId = theResource.getUserData(RESOURCE_PID); 777 JpaPid retVal; 778 if (resourceId == null) { 779 IIdType id = theResource.getIdElement(); 780 try { 781 retVal = resolveResourceIdentityPid( 782 theRequestPartitionId, 783 id.getResourceType(), 784 id.getIdPart(), 785 ResolveIdentityMode.includeDeleted().cacheOk()); 786 } catch (ResourceNotFoundException e) { 787 retVal = null; 788 } 789 } else { 790 retVal = JpaPid.fromId(Long.parseLong(resourceId.toString())); 791 } 792 return retVal; 793 } 794 795 @Override 796 @Nonnull 797 public JpaPid getPidOrThrowException(@Nonnull RequestPartitionId theRequestPartitionId, IIdType theId) { 798 List<IIdType> ids = Collections.singletonList(theId); 799 List<JpaPid> resourcePersistentIds = resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); 800 if (resourcePersistentIds.isEmpty()) { 801 throw new InvalidRequestException(Msg.code(2295) + "Invalid ID was provided: [" + theId.getIdPart() + "]"); 802 } 803 return resourcePersistentIds.get(0); 804 } 805 806 @Override 807 @Nonnull 808 public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) { 809 Long theResourcePID = (Long) theResource.getUserData(RESOURCE_PID); 810 if (theResourcePID == null) { 811 throw new IllegalStateException(Msg.code(2108) 812 + String.format( 813 "Unable to find %s in the user data for %s with ID %s", 814 RESOURCE_PID, theResource, theResource.getId())); 815 } 816 return JpaPid.fromId(theResourcePID); 817 } 818 819 @Override 820 public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) { 821 Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid.getId()); 822 if (optionalResource.isEmpty()) { 823 throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found"); 824 } 825 return optionalResource.get().getIdDt().toVersionless(); 826 } 827 828 /** 829 * Given a set of PIDs, return a set of public FHIR Resource IDs. 830 * 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 831 * Example: 832 * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows: 833 * <p> 834 * [1,2,3] -> ["1","pat1","3"] 835 * 836 * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs. 837 * @return A Set of strings representing the FHIR IDs of the pids. 838 */ 839 @Override 840 public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) { 841 assert TransactionSynchronizationManager.isSynchronizationActive(); 842 843 PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids); 844 845 return pidToForcedIdMap.getResolvedResourceIds(); 846 } 847 848 @Override 849 public JpaPid newPid(Object thePid) { 850 return JpaPid.fromId((Long) thePid); 851 } 852 853 @Override 854 public JpaPid newPidFromStringIdAndResourceName(String thePid, String theResourceName) { 855 return JpaPid.fromIdAndResourceType(Long.parseLong(thePid), theResourceName); 856 } 857 858 private IIdType newIdType(String theValue) { 859 IIdType retVal = myFhirCtx.getVersion().newIdType(); 860 retVal.setValue(theValue); 861 return retVal; 862 } 863 864 public static boolean isValidPid(IIdType theId) { 865 if (theId == null) { 866 return false; 867 } 868 869 String idPart = theId.getIdPart(); 870 return isValidPid(idPart); 871 } 872 873 public static boolean isValidPid(String theIdPart) { 874 return StringUtils.isNumeric(theIdPart); 875 } 876}