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.PartitionablePartitionId; 034import ca.uhn.fhir.jpa.model.entity.ResourceTable; 035import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 036import ca.uhn.fhir.jpa.util.MemoryCacheService; 037import ca.uhn.fhir.jpa.util.QueryChunker; 038import ca.uhn.fhir.model.primitive.IdDt; 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.ResourceNotFoundException; 043import com.google.common.annotations.VisibleForTesting; 044import com.google.common.collect.ListMultimap; 045import com.google.common.collect.MultimapBuilder; 046import jakarta.annotation.Nonnull; 047import jakarta.annotation.Nullable; 048import jakarta.persistence.EntityManager; 049import jakarta.persistence.PersistenceContext; 050import jakarta.persistence.PersistenceContextType; 051import jakarta.persistence.Tuple; 052import jakarta.persistence.TypedQuery; 053import jakarta.persistence.criteria.CriteriaBuilder; 054import jakarta.persistence.criteria.CriteriaQuery; 055import jakarta.persistence.criteria.Predicate; 056import jakarta.persistence.criteria.Root; 057import org.apache.commons.lang3.StringUtils; 058import org.apache.commons.lang3.Validate; 059import org.hl7.fhir.instance.model.api.IAnyResource; 060import org.hl7.fhir.instance.model.api.IBaseResource; 061import org.hl7.fhir.instance.model.api.IIdType; 062import org.hl7.fhir.r4.model.IdType; 063import org.springframework.beans.factory.annotation.Autowired; 064import org.springframework.stereotype.Service; 065import org.springframework.transaction.support.TransactionSynchronizationManager; 066 067import java.time.LocalDate; 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 public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0]; 104 public static final String RESOURCE_PID = "RESOURCE_PID"; 105 106 @Autowired 107 protected IResourceTableDao myResourceTableDao; 108 109 @Autowired 110 private JpaStorageSettings myStorageSettings; 111 112 @Autowired 113 private FhirContext myFhirCtx; 114 115 @Autowired 116 private MemoryCacheService myMemoryCacheService; 117 118 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 119 private EntityManager myEntityManager; 120 121 @Autowired 122 private PartitionSettings myPartitionSettings; 123 124 private boolean myDontCheckActiveTransactionForUnitTest; 125 126 @VisibleForTesting 127 protected void setDontCheckActiveTransactionForUnitTest(boolean theDontCheckActiveTransactionForUnitTest) { 128 myDontCheckActiveTransactionForUnitTest = theDontCheckActiveTransactionForUnitTest; 129 } 130 131 /** 132 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 133 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 134 * 135 * @throws ResourceNotFoundException If the ID can not be found 136 */ 137 @Override 138 @Nonnull 139 public IResourceLookup<JpaPid> resolveResourceIdentity( 140 @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId) 141 throws ResourceNotFoundException { 142 return resolveResourceIdentity(theRequestPartitionId, theResourceType, theResourceId, false); 143 } 144 145 /** 146 * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to 147 * convert those to the underlying Long values that are stored, for lookup and comparison purposes. 148 * Optionally filters out deleted resources. 149 * 150 * @throws ResourceNotFoundException If the ID can not be found 151 */ 152 @Override 153 @Nonnull 154 public IResourceLookup<JpaPid> resolveResourceIdentity( 155 @Nonnull RequestPartitionId theRequestPartitionId, 156 String theResourceType, 157 final String theResourceId, 158 boolean theExcludeDeleted) 159 throws ResourceNotFoundException { 160 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive() 161 : "no transaction active"; 162 163 String resourceIdToUse = theResourceId; 164 if (resourceIdToUse.contains("/")) { 165 resourceIdToUse = theResourceId.substring(resourceIdToUse.indexOf("/") + 1); 166 } 167 IdDt id = new IdDt(theResourceType, resourceIdToUse); 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(resourceIdToUse)) { 173 throw new ResourceNotFoundException(Msg.code(2001) + "Resource " + id + " is not known"); 174 } 175 176 if (matches.size() > 1 || matches.get(resourceIdToUse).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(resourceIdToUse).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(IIdType::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 Integer partitionId = (Integer) next[4]; 581 LocalDate partitionDate = (LocalDate) next[5]; 582 583 JpaResourceLookup lookup = new JpaResourceLookup( 584 resourceType, 585 resourcePid, 586 deletedAt, 587 PartitionablePartitionId.with(partitionId, partitionDate)); 588 retVal.computeIfAbsent(forcedId, id -> new ArrayList<>()).add(lookup); 589 590 if (!myStorageSettings.isDeleteEnabled()) { 591 String key = resourceType + "/" + forcedId; 592 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, key, lookup); 593 } 594 } 595 } 596 } 597 598 return retVal; 599 } 600 601 public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) { 602 if (myPartitionSettings.getDefaultPartitionId() != null) { 603 if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) { 604 List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream() 605 .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t) 606 .collect(Collectors.toList()); 607 return RequestPartitionId.fromPartitionIds(partitionIds); 608 } 609 } 610 return theRequestPartitionId; 611 } 612 613 private void resolvePids( 614 @Nonnull RequestPartitionId theRequestPartitionId, 615 List<Long> thePidsToResolve, 616 Map<String, List<IResourceLookup<JpaPid>>> theTargets) { 617 if (!myStorageSettings.isDeleteEnabled()) { 618 for (Iterator<Long> forcedIdIterator = thePidsToResolve.iterator(); forcedIdIterator.hasNext(); ) { 619 Long nextPid = forcedIdIterator.next(); 620 String nextKey = Long.toString(nextPid); 621 IResourceLookup<JpaPid> cachedLookup = 622 myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey); 623 if (cachedLookup != null) { 624 forcedIdIterator.remove(); 625 theTargets.computeIfAbsent(nextKey, id -> new ArrayList<>()).add(cachedLookup); 626 } 627 } 628 } 629 630 if (!thePidsToResolve.isEmpty()) { 631 Collection<Object[]> lookup; 632 if (theRequestPartitionId.isAllPartitions()) { 633 lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve); 634 } else { 635 if (theRequestPartitionId.isDefaultPartition()) { 636 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve); 637 } else if (theRequestPartitionId.hasDefaultPartitionId()) { 638 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIdsOrNullPartition( 639 thePidsToResolve, theRequestPartitionId.getPartitionIdsWithoutDefault()); 640 } else { 641 lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIds( 642 thePidsToResolve, theRequestPartitionId.getPartitionIds()); 643 } 644 } 645 lookup.stream() 646 .map(t -> new JpaResourceLookup( 647 (String) t[0], 648 (Long) t[1], 649 (Date) t[2], 650 PartitionablePartitionId.with((Integer) t[3], (LocalDate) t[4]))) 651 .forEach(t -> { 652 String id = t.getPersistentId().toString(); 653 if (!theTargets.containsKey(id)) { 654 theTargets.put(id, new ArrayList<>()); 655 } 656 theTargets.get(id).add(t); 657 if (!myStorageSettings.isDeleteEnabled()) { 658 String nextKey = t.getPersistentId().toString(); 659 myMemoryCacheService.putAfterCommit( 660 MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, t); 661 } 662 }); 663 } 664 } 665 666 @Override 667 public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) { 668 assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive(); 669 Set<Long> thePids = theResourceIds.stream().map(JpaPid::getId).collect(Collectors.toSet()); 670 Map<Long, Optional<String>> retVal = new HashMap<>( 671 myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, thePids)); 672 673 List<Long> remainingPids = 674 thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 675 676 new QueryChunker<Long>().chunk(remainingPids, t -> { 677 List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t); 678 679 for (ResourceTable nextResourceEntity : resourceEntities) { 680 Long nextResourcePid = nextResourceEntity.getId(); 681 Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId()); 682 retVal.put(nextResourcePid, nextForcedId); 683 myMemoryCacheService.putAfterCommit( 684 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId); 685 } 686 }); 687 688 remainingPids = thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList()); 689 for (Long nextResourcePid : remainingPids) { 690 retVal.put(nextResourcePid, Optional.empty()); 691 myMemoryCacheService.putAfterCommit( 692 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty()); 693 } 694 Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>(); 695 retVal.forEach((k, v) -> convertRetVal.put(JpaPid.fromId(k), v)); 696 697 return new PersistentIdToForcedIdMap<>(convertRetVal); 698 } 699 700 /** 701 * Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods 702 */ 703 @Override 704 public void addResolvedPidToForcedId( 705 JpaPid theJpaPid, 706 @Nonnull RequestPartitionId theRequestPartitionId, 707 String theResourceType, 708 @Nullable String theForcedId, 709 @Nullable Date theDeletedAt) { 710 if (theForcedId != null) { 711 if (theJpaPid.getAssociatedResourceId() == null) { 712 populateAssociatedResourceId(theResourceType, theForcedId, theJpaPid); 713 } 714 715 myMemoryCacheService.putAfterCommit( 716 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, 717 theJpaPid.getId(), 718 Optional.of(theResourceType + "/" + theForcedId)); 719 String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theForcedId); 720 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theJpaPid); 721 } else { 722 myMemoryCacheService.putAfterCommit( 723 MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theJpaPid.getId(), Optional.empty()); 724 } 725 726 if (!myStorageSettings.isDeleteEnabled()) { 727 JpaResourceLookup lookup = new JpaResourceLookup( 728 theResourceType, theJpaPid.getId(), theDeletedAt, theJpaPid.getPartitionablePartitionId()); 729 String nextKey = theJpaPid.toString(); 730 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, lookup); 731 } 732 } 733 734 @VisibleForTesting 735 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 736 myPartitionSettings = thePartitionSettings; 737 } 738 739 public static boolean isValidPid(IIdType theId) { 740 if (theId == null) { 741 return false; 742 } 743 744 String idPart = theId.getIdPart(); 745 return isValidPid(idPart); 746 } 747 748 public static boolean isValidPid(String theIdPart) { 749 return StringUtils.isNumeric(theIdPart); 750 } 751 752 @Override 753 @Nonnull 754 public List<JpaPid> getPidsOrThrowException( 755 @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds) { 756 return resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds); 757 } 758 759 @Override 760 @Nullable 761 public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) { 762 Object resourceId = theResource.getUserData(RESOURCE_PID); 763 JpaPid retVal; 764 if (resourceId == null) { 765 IIdType id = theResource.getIdElement(); 766 try { 767 retVal = resolveResourcePersistentIds(theRequestPartitionId, id.getResourceType(), id.getIdPart()); 768 } catch (ResourceNotFoundException e) { 769 retVal = null; 770 } 771 } else { 772 retVal = JpaPid.fromId(Long.parseLong(resourceId.toString())); 773 } 774 return retVal; 775 } 776 777 @Override 778 @Nonnull 779 public JpaPid getPidOrThrowException(@Nonnull RequestPartitionId theRequestPartitionId, IIdType theId) { 780 List<IIdType> ids = Collections.singletonList(theId); 781 List<JpaPid> resourcePersistentIds = resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids); 782 if (resourcePersistentIds.isEmpty()) { 783 throw new InvalidRequestException(Msg.code(2295) + "Invalid ID was provided: [" + theId.getIdPart() + "]"); 784 } 785 return resourcePersistentIds.get(0); 786 } 787 788 @Override 789 @Nonnull 790 public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) { 791 Long theResourcePID = (Long) theResource.getUserData(RESOURCE_PID); 792 if (theResourcePID == null) { 793 throw new IllegalStateException(Msg.code(2108) 794 + String.format( 795 "Unable to find %s in the user data for %s with ID %s", 796 RESOURCE_PID, theResource, theResource.getId())); 797 } 798 return JpaPid.fromId(theResourcePID); 799 } 800 801 @Override 802 public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) { 803 Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid.getId()); 804 if (optionalResource.isEmpty()) { 805 throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found"); 806 } 807 return optionalResource.get().getIdDt().toVersionless(); 808 } 809 810 /** 811 * Given a set of PIDs, return a set of public FHIR Resource IDs. 812 * 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 813 * Example: 814 * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows: 815 * <p> 816 * [1,2,3] -> ["1","pat1","3"] 817 * 818 * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs. 819 * @return A Set of strings representing the FHIR IDs of the pids. 820 */ 821 @Override 822 public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) { 823 assert TransactionSynchronizationManager.isSynchronizationActive(); 824 825 PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids); 826 827 return pidToForcedIdMap.getResolvedResourceIds(); 828 } 829 830 @Override 831 public JpaPid newPid(Object thePid) { 832 return JpaPid.fromId((Long) thePid); 833 } 834 835 @Override 836 public JpaPid newPidFromStringIdAndResourceName(String thePid, String theResourceName) { 837 return JpaPid.fromIdAndResourceType(Long.parseLong(thePid), theResourceName); 838 } 839}