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.dao.data.IResourceTableDao; 029import ca.uhn.fhir.jpa.model.config.PartitionSettings; 030import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 031import ca.uhn.fhir.jpa.model.cross.JpaResourceLookup; 032import ca.uhn.fhir.jpa.model.dao.JpaPid; 033import ca.uhn.fhir.jpa.model.entity.ResourceTable; 034import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 035import ca.uhn.fhir.jpa.util.MemoryCacheService; 036import ca.uhn.fhir.jpa.util.QueryChunker; 037import ca.uhn.fhir.model.primitive.IdDt; 038import ca.uhn.fhir.rest.api.server.storage.BaseResourcePersistentId; 039import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 040import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 041import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 042import com.google.common.annotations.VisibleForTesting; 043import com.google.common.collect.ListMultimap; 044import com.google.common.collect.MultimapBuilder; 045import jakarta.annotation.Nonnull; 046import jakarta.annotation.Nullable; 047import jakarta.persistence.EntityManager; 048import jakarta.persistence.PersistenceContext; 049import jakarta.persistence.PersistenceContextType; 050import jakarta.persistence.Tuple; 051import jakarta.persistence.TypedQuery; 052import jakarta.persistence.criteria.CriteriaBuilder; 053import jakarta.persistence.criteria.CriteriaQuery; 054import jakarta.persistence.criteria.Predicate; 055import jakarta.persistence.criteria.Root; 056import org.apache.commons.lang3.StringUtils; 057import org.apache.commons.lang3.Validate; 058import org.hl7.fhir.instance.model.api.IAnyResource; 059import org.hl7.fhir.instance.model.api.IBaseResource; 060import org.hl7.fhir.instance.model.api.IIdType; 061import org.hl7.fhir.r4.model.IdType; 062import org.slf4j.Logger; 063import org.slf4j.LoggerFactory; 064import org.springframework.beans.factory.annotation.Autowired; 065import org.springframework.stereotype.Service; 066import org.springframework.transaction.support.TransactionSynchronizationManager; 067 068import java.util.ArrayList; 069import java.util.Collection; 070import java.util.Collections; 071import java.util.Date; 072import java.util.HashMap; 073import java.util.HashSet; 074import java.util.Iterator; 075import java.util.List; 076import java.util.Map; 077import java.util.Optional; 078import java.util.Set; 079import java.util.stream.Collectors; 080 081import static ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder.replaceDefaultPartitionIdIfNonNull; 082import static org.apache.commons.lang3.StringUtils.isNotBlank; 083 084/** 085 * This class is used to convert between PIDs (the internal primary key for a particular resource as 086 * stored in the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE} table), and the 087 * public ID that a resource has. 088 * <p> 089 * These IDs are sometimes one and the same (by default, a resource that the server assigns the ID of 090 * <code>Patient/1</code> will simply use a PID of 1 and and ID of 1. However, they may also be different 091 * in cases where a forced ID is used (an arbitrary client-assigned ID). 092 * </p> 093 * <p> 094 * This service is highly optimized in order to minimize the number of DB calls as much as possible, 095 * since ID resolution is fundamental to many basic operations. This service returns either 096 * {@link IResourceLookup} or {@link BaseResourcePersistentId} depending on the method being called. 097 * The former involves an extra database join that the latter does not require, so selecting the 098 * right method here is important. 099 * </p> 100 */ 101@Service 102public class IdHelperService implements IIdHelperService<JpaPid> { 103 private static final Logger ourLog = LoggerFactory.getLogger(IdHelperService.class); 104 public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0]; 105 public static final String RESOURCE_PID = "RESOURCE_PID"; 106 107 @Autowired 108 protected IResourceTableDao myResourceTableDao; 109 110 @Autowired 111 private JpaStorageSettings myStorageSettings; 112 113 @Autowired 114 private FhirContext myFhirCtx; 115 116 @Autowired 117 private MemoryCacheService myMemoryCacheService; 118 119 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 120 private EntityManager myEntityManager; 121 122 @Autowired 123 private PartitionSettings myPartitionSettings; 124 125 private boolean myDontCheckActiveTransactionForUnitTest; 126 127 @VisibleForTesting 128 void setDontCheckActiveTransactionForUnitTest(boolean theDontCheckActiveTransactionForUnitTest) { 129 myDontCheckActiveTransactionForUnitTest = theDontCheckActiveTransactionForUnitTest; 130 } 131 132 /** 133 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 134 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 135 * 136 * @throws ResourceNotFoundException If the ID can not be found 137 */ 138 @Override 139 @Nonnull 140 public IResourceLookup<JpaPid> resolveResourceIdentity( 141 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId) 142 throws ResourceNotFoundException { 143 return resolveResourceIdentity(theRequestPartitionId, theResourceType, theResourceId, false); 144 } 145 146 /** 147 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 148 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 149 * Optionally filters out deleted resources. 150 * 151 * @throws ResourceNotFoundException If the ID can not be found 152 */ 153 @Override 154 @Nonnull 155 public IResourceLookup<JpaPid> resolveResourceIdentity( 156 @Nonnull RequestPartitionId theRequestPartitionId, 157 String theResourceType, 158 String theResourceId, 159 boolean theExcludeDeleted) 160 throws ResourceNotFoundException { 161 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive() 162 : "no transaction active"; 163 164 if (theResourceId.contains("/")) { 165 theResourceId = theResourceId.substring(theResourceId.indexOf("/") + 1); 166 } 167 IdDt id = new IdDt(theResourceType, theResourceId); 168 Map<String, List<IResourceLookup<JpaPid>>> matches = 169 translateForcedIdToPids(theRequestPartitionId, Collections.singletonList(id), theExcludeDeleted); 170 171 // We only pass 1 input in so only 0..1 will come back 172 if (matches.isEmpty() || !matches.containsKey(theResourceId)) { 173 throw new ResourceNotFoundException(Msg.code(2001) + "Resource " + id + " is not known"); 174 } 175 176 if (matches.size() > 1 || matches.get(theResourceId).size() > 1) { 177 /* 178 * This means that: 179 * 1. There are two resources with the exact same resource type and forced id 180 * 2. The unique constraint on this column-pair has been dropped 181 */ 182 String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId"); 183 throw new PreconditionFailedException(Msg.code(1099) + msg); 184 } 185 186 return matches.get(theResourceId).get(0); 187 } 188 189 /** 190 * Returns a mapping of Id -> IResourcePersistentId. 191 * If any resource is not found, it will throw ResourceNotFound exception (and no map will be returned) 192 */ 193 @Override 194 @Nonnull 195 public Map<String, JpaPid> resolveResourcePersistentIds( 196 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, List<String> theIds) { 197 return resolveResourcePersistentIds(theRequestPartitionId, theResourceType, theIds, false); 198 } 199 200 /** 201 * Returns a mapping of Id -> IResourcePersistentId. 202 * If any resource is not found, it will throw ResourceNotFound exception (and no map will be returned) 203 * Optionally filters out deleted resources. 204 */ 205 @Override 206 @Nonnull 207 public Map<String, JpaPid> resolveResourcePersistentIds( 208 @Nonnull RequestPartitionId theRequestPartitionId, 209 String theResourceType, 210 List<String> theIds, 211 boolean theExcludeDeleted) { 212 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 213 Validate.notNull(theIds, "theIds cannot be null"); 214 Validate.isTrue(!theIds.isEmpty(), "theIds must not be empty"); 215 216 Map<String, JpaPid> retVals = new HashMap<>(); 217 for (String id : theIds) { 218 JpaPid retVal; 219 if (!idRequiresForcedId(id)) { 220 // is already a PID 221 retVal = JpaPid.fromId(Long.parseLong(id)); 222 retVals.put(id, retVal); 223 } else { 224 // is a forced id 225 // we must resolve! 226 if (myStorageSettings.isDeleteEnabled()) { 227 retVal = resolveResourceIdentity(theRequestPartitionId, theResourceType, id, theExcludeDeleted) 228 .getPersistentId(); 229 retVals.put(id, retVal); 230 } else { 231 // fetch from cache... adding to cache if not available 232 String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, id); 233 retVal = myMemoryCacheService.getThenPutAfterCommit( 234 MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, t -> { 235 List<IIdType> ids = Collections.singletonList(new IdType(theResourceType, id)); 236 // fetches from cache using a function that checks cache first... 237 List<JpaPid> resolvedIds = 238 resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); 239 if (resolvedIds.isEmpty()) { 240 throw new ResourceNotFoundException(Msg.code(1100) + ids.get(0)); 241 } 242 return resolvedIds.get(0); 243 }); 244 retVals.put(id, retVal); 245 } 246 } 247 } 248 249 return retVals; 250 } 251 252 /** 253 * Given a resource type and ID, determines the internal persistent ID for the resource. 254 * 255 * @throws ResourceNotFoundException If the ID can not be found 256 */ 257 @Override 258 @Nonnull 259 public JpaPid resolveResourcePersistentIds( 260 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theId) { 261 return resolveResourcePersistentIds(theRequestPartitionId, theResourceType, theId, false); 262 } 263 264 /** 265 * Given a resource type and ID, determines the internal persistent ID for the resource. 266 * Optionally filters out deleted resources. 267 * 268 * @throws ResourceNotFoundException If the ID can not be found 269 */ 270 @Nonnull 271 @Override 272 public JpaPid resolveResourcePersistentIds( 273 @Nonnull RequestPartitionId theRequestPartitionId, 274 String theResourceType, 275 String theId, 276 boolean theExcludeDeleted) { 277 Validate.notNull(theId, "theId must not be null"); 278 279 Map<String, JpaPid> retVal = resolveResourcePersistentIds( 280 theRequestPartitionId, theResourceType, Collections.singletonList(theId), theExcludeDeleted); 281 return retVal.get(theId); // should be only one 282 } 283 284 /** 285 * Returns true if the given resource ID should be stored in a forced ID. Under default config 286 * (meaning client ID strategy is {@link JpaStorageSettings.ClientIdStrategyEnum#ALPHANUMERIC}) 287 * this will return true if the ID has any non-digit characters. 288 * <p> 289 * In {@link JpaStorageSettings.ClientIdStrategyEnum#ANY} mode it will always return true. 290 */ 291 @Override 292 public boolean idRequiresForcedId(String theId) { 293 return myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ANY 294 || !isValidPid(theId); 295 } 296 297 @Nonnull 298 private String toForcedIdToPidKey( 299 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theId) { 300 return RequestPartitionId.stringifyForKey(theRequestPartitionId) + "/" + theResourceType + "/" + theId; 301 } 302 303 /** 304 * Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs. 305 * <p> 306 * This implementation will always try to use a cache for performance, meaning that it can resolve resources that 307 * are deleted (but note that forced IDs can't change, so the cache can't return incorrect results) 308 */ 309 @Override 310 @Nonnull 311 public List<JpaPid> resolveResourcePersistentIdsWithCache( 312 RequestPartitionId theRequestPartitionId, List<IIdType> theIds) { 313 boolean onlyForcedIds = false; 314 return resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds, onlyForcedIds); 315 } 316 317 /** 318 * Given a collection of resource IDs (resource type + id), resolves the internal persistent IDs. 319 * <p> 320 * This implementation will always try to use a cache for performance, meaning that it can resolve resources that 321 * are deleted (but note that forced IDs can't change, so the cache can't return incorrect results) 322 * 323 * @param theOnlyForcedIds If <code>true</code>, resources which are not existing forced IDs will not be resolved 324 */ 325 @Override 326 @Nonnull 327 public List<JpaPid> resolveResourcePersistentIdsWithCache( 328 @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds, boolean theOnlyForcedIds) { 329 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 330 331 List<JpaPid> retVal = new ArrayList<>(theIds.size()); 332 333 for (IIdType id : theIds) { 334 if (!id.hasIdPart()) { 335 throw new InvalidRequestException(Msg.code(1101) + "Parameter value missing in request"); 336 } 337 } 338 339 if (!theIds.isEmpty()) { 340 Set<IIdType> idsToCheck = new HashSet<>(theIds.size()); 341 for (IIdType nextId : theIds) { 342 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY) { 343 if (nextId.isIdPartValidLong()) { 344 if (!theOnlyForcedIds) { 345 JpaPid jpaPid = JpaPid.fromId(nextId.getIdPartAsLong()); 346 jpaPid.setAssociatedResourceId(nextId); 347 retVal.add(jpaPid); 348 } 349 continue; 350 } 351 } 352 353 String key = toForcedIdToPidKey(theRequestPartitionId, nextId.getResourceType(), nextId.getIdPart()); 354 JpaPid cachedId = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key); 355 if (cachedId != null) { 356 retVal.add(cachedId); 357 continue; 358 } 359 360 idsToCheck.add(nextId); 361 } 362 new QueryChunker<IIdType>() 363 .chunk( 364 idsToCheck, 365 SearchBuilder.getMaximumPageSize() / 2, 366 ids -> doResolvePersistentIds(theRequestPartitionId, ids, retVal)); 367 } 368 369 return retVal; 370 } 371 372 private void doResolvePersistentIds( 373 RequestPartitionId theRequestPartitionId, List<IIdType> theIds, List<JpaPid> theOutputListToPopulate) { 374 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 375 CriteriaQuery<Tuple> criteriaQuery = cb.createTupleQuery(); 376 Root<ResourceTable> from = criteriaQuery.from(ResourceTable.class); 377 378 /* 379 * IDX_RES_FHIR_ID covers these columns, but RES_ID is only INCLUDEd. 380 * Only PG, and MSSql support INCLUDE COLUMNS. 381 * @see AddIndexTask.generateSql 382 */ 383 criteriaQuery.multiselect(from.get("myId"), from.get("myResourceType"), from.get("myFhirId")); 384 385 // one create one clause per id. 386 List<Predicate> predicates = new ArrayList<>(theIds.size()); 387 for (IIdType next : theIds) { 388 389 List<Predicate> andPredicates = new ArrayList<>(3); 390 391 if (isNotBlank(next.getResourceType())) { 392 Predicate typeCriteria = cb.equal(from.get("myResourceType"), next.getResourceType()); 393 andPredicates.add(typeCriteria); 394 } 395 396 Predicate idCriteria = cb.equal(from.get("myFhirId"), next.getIdPart()); 397 andPredicates.add(idCriteria); 398 getOptionalPartitionPredicate(theRequestPartitionId, cb, from).ifPresent(andPredicates::add); 399 predicates.add(cb.and(andPredicates.toArray(EMPTY_PREDICATE_ARRAY))); 400 } 401 402 // join all the clauses as OR 403 criteriaQuery.where(cb.or(predicates.toArray(EMPTY_PREDICATE_ARRAY))); 404 405 TypedQuery<Tuple> query = myEntityManager.createQuery(criteriaQuery); 406 List<Tuple> results = query.getResultList(); 407 for (Tuple nextId : results) { 408 // Check if the nextId has a resource ID. It may have a null resource ID if a commit is still pending. 409 Long resourceId = nextId.get(0, Long.class); 410 String resourceType = nextId.get(1, String.class); 411 String forcedId = nextId.get(2, String.class); 412 if (resourceId != null) { 413 JpaPid jpaPid = JpaPid.fromId(resourceId); 414 populateAssociatedResourceId(resourceType, forcedId, jpaPid); 415 theOutputListToPopulate.add(jpaPid); 416 417 String key = toForcedIdToPidKey(theRequestPartitionId, resourceType, forcedId); 418 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, jpaPid); 419 } 420 } 421 } 422 423 /** 424 * Return optional predicate for searching on forcedId 425 * 1. If the partition mode is ALLOWED_UNQUALIFIED, the return optional predicate will be empty, so search is across all partitions. 426 * 2. If it is default partition and default partition id is null, then return predicate for null partition. 427 * 3. If the requested partition search is not all partition, return the request partition as predicate. 428 */ 429 private Optional<Predicate> getOptionalPartitionPredicate( 430 RequestPartitionId theRequestPartitionId, CriteriaBuilder cb, Root<ResourceTable> from) { 431 if (myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()) { 432 return Optional.empty(); 433 } else if (theRequestPartitionId.isDefaultPartition() && myPartitionSettings.getDefaultPartitionId() == null) { 434 Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue")); 435 return Optional.of(partitionIdCriteria); 436 } else if (!theRequestPartitionId.isAllPartitions()) { 437 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds(); 438 partitionIds = replaceDefaultPartitionIdIfNonNull(myPartitionSettings, partitionIds); 439 if (partitionIds.size() > 1) { 440 Predicate partitionIdCriteria = from.get("myPartitionIdValue").in(partitionIds); 441 return Optional.of(partitionIdCriteria); 442 } else if (partitionIds.size() == 1) { 443 Predicate partitionIdCriteria = cb.equal(from.get("myPartitionIdValue"), partitionIds.get(0)); 444 return Optional.of(partitionIdCriteria); 445 } 446 } 447 return Optional.empty(); 448 } 449 450 private void populateAssociatedResourceId(String nextResourceType, String forcedId, JpaPid jpaPid) { 451 IIdType resourceId = myFhirCtx.getVersion().newIdType(); 452 resourceId.setValue(nextResourceType + "/" + forcedId); 453 jpaPid.setAssociatedResourceId(resourceId); 454 } 455 456 /** 457 * Given a persistent ID, returns the associated resource ID 458 */ 459 @Nonnull 460 @Override 461 public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, JpaPid theId) { 462 if (theId.getAssociatedResourceId() != null) { 463 return theId.getAssociatedResourceId(); 464 } 465 466 IIdType retVal = theCtx.getVersion().newIdType(); 467 468 Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId); 469 if (forcedId.isPresent()) { 470 retVal.setValue(forcedId.get()); 471 } else { 472 retVal.setValue(theResourceType + '/' + theId); 473 } 474 475 return retVal; 476 } 477 478 @Override 479 public Optional<String> translatePidIdToForcedIdWithCache(JpaPid theId) { 480 // do getIfPresent and then put to avoid doing I/O inside the cache. 481 Optional<String> forcedId = 482 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId()); 483 484 if (forcedId == null) { 485 // This is only called when we know the resource exists. 486 // So this optional is only empty when there is no hfj_forced_id table 487 // note: this is obsolete with the new fhir_id column, and will go away. 488 forcedId = myResourceTableDao.findById(theId.getId()).map(ResourceTable::asTypedFhirResourceId); 489 myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId.getId(), forcedId); 490 } 491 492 return forcedId; 493 } 494 495 private ListMultimap<String, String> organizeIdsByResourceType(Collection<IIdType> theIds) { 496 ListMultimap<String, String> typeToIds = 497 MultimapBuilder.hashKeys().arrayListValues().build(); 498 for (IIdType nextId : theIds) { 499 if (myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ANY 500 || !isValidPid(nextId)) { 501 if (nextId.hasResourceType()) { 502 typeToIds.put(nextId.getResourceType(), nextId.getIdPart()); 503 } else { 504 typeToIds.put("", nextId.getIdPart()); 505 } 506 } 507 } 508 return typeToIds; 509 } 510 511 private Map<String, List<IResourceLookup<JpaPid>>> translateForcedIdToPids( 512 @Nonnull RequestPartitionId theRequestPartitionId, Collection<IIdType> theId, boolean theExcludeDeleted) { 513 theId.forEach(id -> Validate.isTrue(id.hasIdPart())); 514 515 if (theId.isEmpty()) { 516 return new HashMap<>(); 517 } 518 519 Map<String, List<IResourceLookup<JpaPid>>> retVal = new HashMap<>(); 520 RequestPartitionId requestPartitionId = replaceDefault(theRequestPartitionId); 521 522 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY) { 523 List<Long> pids = theId.stream() 524 .filter(t -> isValidPid(t)) 525 .map(t -> t.getIdPartAsLong()) 526 .collect(Collectors.toList()); 527 if (!pids.isEmpty()) { 528 resolvePids(requestPartitionId, pids, retVal); 529 } 530 } 531 532 // returns a map of resourcetype->id 533 ListMultimap<String, String> typeToIds = organizeIdsByResourceType(theId); 534 for (Map.Entry<String, Collection<String>> nextEntry : typeToIds.asMap().entrySet()) { 535 String nextResourceType = nextEntry.getKey(); 536 Collection<String> nextIds = nextEntry.getValue(); 537 538 if (!myStorageSettings.isDeleteEnabled()) { 539 for (Iterator<String> forcedIdIterator = nextIds.iterator(); forcedIdIterator.hasNext(); ) { 540 String nextForcedId = forcedIdIterator.next(); 541 String nextKey = nextResourceType + "/" + nextForcedId; 542 IResourceLookup<JpaPid> cachedLookup = 543 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey); 544 if (cachedLookup != null) { 545 forcedIdIterator.remove(); 546 retVal.computeIfAbsent(nextForcedId, id -> new ArrayList<>()) 547 .add(cachedLookup); 548 } 549 } 550 } 551 552 if (!nextIds.isEmpty()) { 553 Collection<Object[]> views; 554 assert isNotBlank(nextResourceType); 555 556 if (requestPartitionId.isAllPartitions()) { 557 views = myResourceTableDao.findAndResolveByForcedIdWithNoType( 558 nextResourceType, nextIds, theExcludeDeleted); 559 } else { 560 if (requestPartitionId.isDefaultPartition()) { 561 views = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionNull( 562 nextResourceType, nextIds, theExcludeDeleted); 563 } else if (requestPartitionId.hasDefaultPartitionId()) { 564 views = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartitionIdOrNullPartitionId( 565 nextResourceType, 566 nextIds, 567 requestPartitionId.getPartitionIdsWithoutDefault(), 568 theExcludeDeleted); 569 } else { 570 views = myResourceTableDao.findAndResolveByForcedIdWithNoTypeInPartition( 571 nextResourceType, nextIds, requestPartitionId.getPartitionIds(), theExcludeDeleted); 572 } 573 } 574 575 for (Object[] next : views) { 576 String resourceType = (String) next[0]; 577 Long resourcePid = (Long) next[1]; 578 String forcedId = (String) next[2]; 579 Date deletedAt = (Date) next[3]; 580 581 JpaResourceLookup lookup = new JpaResourceLookup(resourceType, resourcePid, deletedAt); 582 retVal.computeIfAbsent(forcedId, id -> new ArrayList<>()).add(lookup); 583 584 if (!myStorageSettings.isDeleteEnabled()) { 585 String key = resourceType + "/" + forcedId; 586 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, key, lookup); 587 } 588 } 589 } 590 } 591 592 return retVal; 593 } 594 595 public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) { 596 if (myPartitionSettings.getDefaultPartitionId() != null) { 597 if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) { 598 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream() 599 .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t) 600 .collect(Collectors.toList()); 601 return RequestPartitionId.fromPartitionIds(partitionIds); 602 } 603 } 604 return theRequestPartitionId; 605 } 606 607 private void resolvePids( 608 @Nonnull RequestPartitionId theRequestPartitionId, 609 List<Long> thePidsToResolve, 610 Map<String, List<IResourceLookup<JpaPid>>> theTargets) { 611 if (!myStorageSettings.isDeleteEnabled()) { 612 for (Iterator<Long> forcedIdIterator = thePidsToResolve.iterator(); forcedIdIterator.hasNext(); ) { 613 Long nextPid = forcedIdIterator.next(); 614 String nextKey = Long.toString(nextPid); 615 IResourceLookup<JpaPid> cachedLookup = 616 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey); 617 if (cachedLookup != null) { 618 forcedIdIterator.remove(); 619 theTargets.computeIfAbsent(nextKey, id -> new ArrayList<>()).add(cachedLookup); 620 } 621 } 622 } 623 624 if (!thePidsToResolve.isEmpty()) { 625 Collection<Object[]> lookup; 626 if (theRequestPartitionId.isAllPartitions()) { 627 lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve); 628 } else { 629 if (theRequestPartitionId.isDefaultPartition()) { 630 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve); 631 } else if (theRequestPartitionId.hasDefaultPartitionId()) { 632 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIdsOrNullPartition( 633 thePidsToResolve, theRequestPartitionId.getPartitionIdsWithoutDefault()); 634 } else { 635 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIds( 636 thePidsToResolve, theRequestPartitionId.getPartitionIds()); 637 } 638 } 639 lookup.stream() 640 .map(t -> new JpaResourceLookup((String) t[0], (Long) t[1], (Date) t[2])) 641 .forEach(t -> { 642 String id = t.getPersistentId().toString(); 643 if (!theTargets.containsKey(id)) { 644 theTargets.put(id, new ArrayList<>()); 645 } 646 theTargets.get(id).add(t); 647 if (!myStorageSettings.isDeleteEnabled()) { 648 String nextKey = t.getPersistentId().toString(); 649 myMemoryCacheService.putAfterCommit( 650 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, t); 651 } 652 }); 653 } 654 } 655 656 @Override 657 public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) { 658 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 659 Set<Long> thePids = theResourceIds.stream().map(JpaPid::getId).collect(Collectors.toSet()); 660 Map<Long, Optional<String>> retVal = new HashMap<>( 661 myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, thePids)); 662 663 List<Long> remainingPids = 664 thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 665 666 new QueryChunker<Long>().chunk(remainingPids, t -> { 667 List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t); 668 669 for (ResourceTable nextResourceEntity : resourceEntities) { 670 Long nextResourcePid = nextResourceEntity.getId(); 671 Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId()); 672 retVal.put(nextResourcePid, nextForcedId); 673 myMemoryCacheService.putAfterCommit( 674 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId); 675 } 676 }); 677 678 remainingPids = thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 679 for (Long nextResourcePid : remainingPids) { 680 retVal.put(nextResourcePid, Optional.empty()); 681 myMemoryCacheService.putAfterCommit( 682 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty()); 683 } 684 Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>(); 685 retVal.forEach((k, v) -> { 686 convertRetVal.put(JpaPid.fromId(k), v); 687 }); 688 return new PersistentIdToForcedIdMap<>(convertRetVal); 689 } 690 691 /** 692 * Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods 693 */ 694 @Override 695 public void addResolvedPidToForcedId( 696 JpaPid theJpaPid, 697 @Nonnull RequestPartitionId theRequestPartitionId, 698 String theResourceType, 699 @Nullable String theForcedId, 700 @Nullable Date theDeletedAt) { 701 if (theForcedId != null) { 702 if (theJpaPid.getAssociatedResourceId() == null) { 703 populateAssociatedResourceId(theResourceType, theForcedId, theJpaPid); 704 } 705 706 myMemoryCacheService.putAfterCommit( 707 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, 708 theJpaPid.getId(), 709 Optional.of(theResourceType + "/" + theForcedId)); 710 String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theForcedId); 711 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theJpaPid); 712 } else { 713 myMemoryCacheService.putAfterCommit( 714 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theJpaPid.getId(), Optional.empty()); 715 } 716 717 if (!myStorageSettings.isDeleteEnabled()) { 718 JpaResourceLookup lookup = new JpaResourceLookup(theResourceType, theJpaPid.getId(), theDeletedAt); 719 String nextKey = theJpaPid.toString(); 720 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, lookup); 721 } 722 } 723 724 @VisibleForTesting 725 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 726 myPartitionSettings = thePartitionSettings; 727 } 728 729 public static boolean isValidPid(IIdType theId) { 730 if (theId == null) { 731 return false; 732 } 733 734 String idPart = theId.getIdPart(); 735 return isValidPid(idPart); 736 } 737 738 public static boolean isValidPid(String theIdPart) { 739 return StringUtils.isNumeric(theIdPart); 740 } 741 742 @Override 743 @Nonnull 744 public List<JpaPid> getPidsOrThrowException( 745 @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds) { 746 List<JpaPid> resourcePersistentIds = resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds); 747 return resourcePersistentIds; 748 } 749 750 @Override 751 @Nullable 752 public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) { 753 Object resourceId = theResource.getUserData(RESOURCE_PID); 754 JpaPid retVal; 755 if (resourceId == null) { 756 IIdType id = theResource.getIdElement(); 757 try { 758 retVal = resolveResourcePersistentIds(theRequestPartitionId, id.getResourceType(), id.getIdPart()); 759 } catch (ResourceNotFoundException e) { 760 retVal = null; 761 } 762 } else { 763 retVal = JpaPid.fromId(Long.parseLong(resourceId.toString())); 764 } 765 return retVal; 766 } 767 768 @Override 769 @Nonnull 770 public JpaPid getPidOrThrowException(@Nonnull RequestPartitionId theRequestPartitionId, IIdType theId) { 771 List<IIdType> ids = Collections.singletonList(theId); 772 List<JpaPid> resourcePersistentIds = resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); 773 if (resourcePersistentIds.isEmpty()) { 774 throw new InvalidRequestException(Msg.code(2295) + "Invalid ID was provided: [" + theId.getIdPart() + "]"); 775 } 776 return resourcePersistentIds.get(0); 777 } 778 779 @Override 780 @Nonnull 781 public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) { 782 Long theResourcePID = (Long) theResource.getUserData(RESOURCE_PID); 783 if (theResourcePID == null) { 784 throw new IllegalStateException(Msg.code(2108) 785 + String.format( 786 "Unable to find %s in the user data for %s with ID %s", 787 RESOURCE_PID, theResource, theResource.getId())); 788 } 789 return JpaPid.fromId(theResourcePID); 790 } 791 792 @Override 793 public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) { 794 Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid.getId()); 795 if (optionalResource.isEmpty()) { 796 throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found"); 797 } 798 return optionalResource.get().getIdDt().toVersionless(); 799 } 800 801 /** 802 * Given a set of PIDs, return a set of public FHIR Resource IDs. 803 * 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 804 * Example: 805 * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows: 806 * <p> 807 * [1,2,3] -> ["1","pat1","3"] 808 * 809 * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs. 810 * @return A Set of strings representing the FHIR IDs of the pids. 811 */ 812 @Override 813 public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) { 814 assert TransactionSynchronizationManager.isSynchronizationActive(); 815 816 PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids); 817 818 return pidToForcedIdMap.getResolvedResourceIds(); 819 } 820 821 @Override 822 public JpaPid newPid(Object thePid) { 823 return JpaPid.fromId((Long) thePid); 824 } 825 826 @Override 827 public JpaPid newPidFromStringIdAndResourceName(String thePid, String theResourceName) { 828 return JpaPid.fromIdAndResourceType(Long.parseLong(thePid), theResourceName); 829 } 830}