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