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 request partition is "all partitions", don't include a predicate.
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 (theRequestPartitionId.isAllPartitions()) {
426                        return Optional.empty();
427                } else {
428                        List<Integer> partitionIds = theRequestPartitionId.getPartitionIds();
429                        partitionIds = replaceDefaultPartitionIdIfNonNull(myPartitionSettings, partitionIds);
430                        if (partitionIds.contains(null)) {
431                                Predicate partitionIdNullCriteria =
432                                                from.get("myPartitionIdValue").isNull();
433                                if (partitionIds.size() == 1) {
434                                        return Optional.of(partitionIdNullCriteria);
435                                } else {
436                                        Predicate partitionIdCriteria = from.get("myPartitionIdValue")
437                                                        .in(partitionIds.stream().filter(Objects::nonNull).collect(Collectors.toList()));
438                                        return Optional.of(cb.or(partitionIdCriteria, partitionIdNullCriteria));
439                                }
440                        } else {
441                                if (partitionIds.size() > 1) {
442                                        Predicate partitionIdCriteria =
443                                                        from.get("myPartitionIdValue").in(partitionIds);
444                                        return Optional.of(partitionIdCriteria);
445                                } else if (partitionIds.size() == 1) {
446                                        Predicate partitionIdCriteria = cb.equal(from.get("myPartitionIdValue"), partitionIds.get(0));
447                                        return Optional.of(partitionIdCriteria);
448                                }
449                        }
450                }
451                return Optional.empty();
452        }
453
454        private void populateAssociatedResourceId(String nextResourceType, String forcedId, JpaPid jpaPid) {
455                IIdType resourceId = myFhirCtx.getVersion().newIdType();
456                resourceId.setValue(nextResourceType + "/" + forcedId);
457                jpaPid.setAssociatedResourceId(resourceId);
458        }
459
460        /**
461         * Given a persistent ID, returns the associated resource ID
462         */
463        @Nonnull
464        @Override
465        public IIdType translatePidIdToForcedId(FhirContext theCtx, String theResourceType, JpaPid theId) {
466                if (theId.getAssociatedResourceId() != null) {
467                        return theId.getAssociatedResourceId();
468                }
469
470                IIdType retVal = theCtx.getVersion().newIdType();
471
472                Optional<String> forcedId = translatePidIdToForcedIdWithCache(theId);
473                if (forcedId.isPresent()) {
474                        retVal.setValue(forcedId.get());
475                } else {
476                        retVal.setValue(theResourceType + '/' + theId.getId());
477                }
478
479                return retVal;
480        }
481
482        @SuppressWarnings("OptionalAssignedToNull")
483        @Override
484        public Optional<String> translatePidIdToForcedIdWithCache(JpaPid theId) {
485                // do getIfPresent and then put to avoid doing I/O inside the cache.
486                Optional<String> forcedId =
487                                myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId);
488
489                if (forcedId == null) {
490                        // This is only called when we know the resource exists.
491                        // So this optional is only empty when there is no hfj_forced_id table
492                        // note: this is obsolete with the new fhir_id column, and will go away.
493                        forcedId = myTransactionService
494                                        .withSystemRequest()
495                                        .withRequestPartitionId(RequestPartitionId.fromPartitionIds(theId.getPartitionId()))
496                                        .execute(() -> myResourceTableDao.findById(theId).map(ResourceTable::asTypedFhirResourceId));
497
498                        myMemoryCacheService.put(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theId, forcedId);
499                }
500
501                return forcedId;
502        }
503
504        public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) {
505                if (myPartitionSettings.getDefaultPartitionId() != null) {
506                        if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) {
507                                List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream()
508                                                .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t)
509                                                .collect(Collectors.toList());
510                                return RequestPartitionId.fromPartitionIds(partitionIds);
511                        }
512                }
513                return theRequestPartitionId;
514        }
515
516        @Override
517        public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) {
518                assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive();
519                HashMap<JpaPid, Optional<String>> retVal = new HashMap<>(
520                                myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theResourceIds));
521
522                List<JpaPid> remainingPids =
523                                theResourceIds.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList());
524
525                QueryChunker.chunk(remainingPids, t -> {
526                        List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t);
527
528                        for (ResourceTable nextResourceEntity : resourceEntities) {
529                                JpaPid nextResourcePid = nextResourceEntity.getPersistentId();
530                                Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId());
531                                retVal.put(nextResourcePid, nextForcedId);
532                                myMemoryCacheService.putAfterCommit(
533                                                MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId);
534                        }
535                });
536
537                remainingPids =
538                                theResourceIds.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList());
539                for (JpaPid nextResourcePid : remainingPids) {
540                        retVal.put(nextResourcePid, Optional.empty());
541                        myMemoryCacheService.putAfterCommit(
542                                        MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty());
543                }
544                Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>(retVal);
545
546                return new PersistentIdToForcedIdMap<>(convertRetVal);
547        }
548
549        /**
550         * This method can be called to pre-emptively add entries to the ID cache. It should
551         * be called by DAO methods if they are creating or changing the deleted status
552         * of a resource. This method returns immediately, but the data is not
553         * added to the internal caches until the current DB transaction is successfully
554         * committed, and nothing is added if the transaction rolls back.
555         */
556        @Override
557        public void addResolvedPidToFhirIdAfterCommit(
558                        @Nonnull JpaPid theJpaPid,
559                        @Nonnull RequestPartitionId theRequestPartitionId,
560                        @Nonnull String theResourceType,
561                        @Nonnull String theFhirId,
562                        @Nullable Date theDeletedAt) {
563                if (theJpaPid.getAssociatedResourceId() == null) {
564                        populateAssociatedResourceId(theResourceType, theFhirId, theJpaPid);
565                }
566
567                myMemoryCacheService.putAfterCommit(
568                                MemoryCacheService.CacheEnum.PID_TO_FORCED_ID,
569                                theJpaPid,
570                                Optional.of(theResourceType + "/" + theFhirId));
571
572                JpaResourceLookup lookup = new JpaResourceLookup(
573                                theResourceType, theFhirId, theJpaPid.getId(), theDeletedAt, theJpaPid.getPartitionablePartitionId());
574
575                MemoryCacheService.ForcedIdCacheKey fhirIdKey =
576                                new MemoryCacheService.ForcedIdCacheKey(theResourceType, theFhirId, theRequestPartitionId);
577                myMemoryCacheService.putAfterCommit(
578                                MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKey, List.of(lookup));
579
580                // If it's a pure-numeric ID, store it in the cache without a type as well
581                // so that we can resolve it this way when loading entities for update
582                if (myStorageSettings.getResourceClientIdStrategy() == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC
583                                && isValidLong(theFhirId)) {
584                        MemoryCacheService.ForcedIdCacheKey fhirIdKeyWithoutType =
585                                        new MemoryCacheService.ForcedIdCacheKey(null, theFhirId, theRequestPartitionId);
586                        myMemoryCacheService.putAfterCommit(
587                                        MemoryCacheService.CacheEnum.RESOURCE_LOOKUP_BY_FORCED_ID, fhirIdKeyWithoutType, List.of(lookup));
588                }
589        }
590
591        @VisibleForTesting
592        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
593                myPartitionSettings = thePartitionSettings;
594        }
595
596        @Override
597        @Nullable
598        public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) {
599                Object resourceId = theResource.getUserData(RESOURCE_PID);
600                JpaPid retVal;
601                if (resourceId == null) {
602                        IIdType id = theResource.getIdElement();
603                        try {
604                                retVal = resolveResourceIdentityPid(
605                                                theRequestPartitionId,
606                                                id.getResourceType(),
607                                                id.getIdPart(),
608                                                ResolveIdentityMode.includeDeleted().cacheOk());
609                        } catch (ResourceNotFoundException e) {
610                                retVal = null;
611                        }
612                } else {
613                        retVal = (JpaPid) resourceId;
614                }
615                return retVal;
616        }
617
618        @Override
619        @Nonnull
620        public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) {
621                JpaPid theResourcePID = (JpaPid) theResource.getUserData(RESOURCE_PID);
622                if (theResourcePID == null) {
623                        throw new IllegalStateException(Msg.code(2108)
624                                        + String.format(
625                                                        "Unable to find %s in the user data for %s with ID %s",
626                                                        RESOURCE_PID, theResource, theResource.getId()));
627                }
628                return theResourcePID;
629        }
630
631        @Override
632        public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) {
633                Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid);
634                if (optionalResource.isEmpty()) {
635                        throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found");
636                }
637                return optionalResource.get().getIdDt().toVersionless();
638        }
639
640        /**
641         * Given a set of PIDs, return a set of public FHIR Resource IDs.
642         * 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
643         * Example:
644         * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows:
645         * <p>
646         * [1,2,3] -> ["1","pat1","3"]
647         *
648         * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs.
649         * @return A Set of strings representing the FHIR IDs of the pids.
650         */
651        @Override
652        public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) {
653                assert TransactionSynchronizationManager.isSynchronizationActive();
654
655                PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids);
656
657                return pidToForcedIdMap.getResolvedResourceIds();
658        }
659
660        @Override
661        public JpaPid newPid(Object thePid) {
662                return JpaPid.fromId((Long) thePid);
663        }
664
665        @Override
666        public JpaPid newPid(Object thePid, Integer thePartitionId) {
667                return JpaPid.fromId((Long) thePid, thePartitionId);
668        }
669
670        @Override
671        public JpaPid newPidFromStringIdAndResourceName(Integer thePartitionId, String thePid, String theResourceName) {
672                JpaPid retVal = JpaPid.fromId(Long.parseLong(thePid), thePartitionId);
673                retVal.setResourceType(theResourceName);
674                return retVal;
675        }
676
677        private IIdType newIdType(String theValue) {
678                IIdType retVal = myFhirCtx.getVersion().newIdType();
679                retVal.setValue(theValue);
680                return retVal;
681        }
682
683        public static boolean isValidPid(String theIdPart) {
684                return StringUtils.isNumeric(theIdPart);
685        }
686}