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