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