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