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.PartitionablePartitionId;
034import ca.uhn.fhir.jpa.model.entity.ResourceTable;
035import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
036import ca.uhn.fhir.jpa.util.MemoryCacheService;
037import ca.uhn.fhir.jpa.util.QueryChunker;
038import ca.uhn.fhir.model.primitive.IdDt;
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.ResourceNotFoundException;
043import com.google.common.annotations.VisibleForTesting;
044import com.google.common.collect.ListMultimap;
045import com.google.common.collect.MultimapBuilder;
046import jakarta.annotation.Nonnull;
047import jakarta.annotation.Nullable;
048import jakarta.persistence.EntityManager;
049import jakarta.persistence.PersistenceContext;
050import jakarta.persistence.PersistenceContextType;
051import jakarta.persistence.Tuple;
052import jakarta.persistence.TypedQuery;
053import jakarta.persistence.criteria.CriteriaBuilder;
054import jakarta.persistence.criteria.CriteriaQuery;
055import jakarta.persistence.criteria.Predicate;
056import jakarta.persistence.criteria.Root;
057import org.apache.commons.lang3.StringUtils;
058import org.apache.commons.lang3.Validate;
059import org.hl7.fhir.instance.model.api.IAnyResource;
060import org.hl7.fhir.instance.model.api.IBaseResource;
061import org.hl7.fhir.instance.model.api.IIdType;
062import org.hl7.fhir.r4.model.IdType;
063import org.springframework.beans.factory.annotation.Autowired;
064import org.springframework.stereotype.Service;
065import org.springframework.transaction.support.TransactionSynchronizationManager;
066
067import java.time.LocalDate;
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        public static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[0];
104        public static final String RESOURCE_PID = "RESOURCE_PID";
105
106        @Autowired
107        protected IResourceTableDao myResourceTableDao;
108
109        @Autowired
110        private JpaStorageSettings myStorageSettings;
111
112        @Autowired
113        private FhirContext myFhirCtx;
114
115        @Autowired
116        private MemoryCacheService myMemoryCacheService;
117
118        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
119        private EntityManager myEntityManager;
120
121        @Autowired
122        private PartitionSettings myPartitionSettings;
123
124        private boolean myDontCheckActiveTransactionForUnitTest;
125
126        @VisibleForTesting
127        protected void setDontCheckActiveTransactionForUnitTest(boolean theDontCheckActiveTransactionForUnitTest) {
128                myDontCheckActiveTransactionForUnitTest = theDontCheckActiveTransactionForUnitTest;
129        }
130
131        /**
132         * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to
133         * convert those to the underlying Long values that are stored, for lookup and comparison purposes.
134         *
135         * @throws ResourceNotFoundException If the ID can not be found
136         */
137        @Override
138        @Nonnull
139        public IResourceLookup<JpaPid> resolveResourceIdentity(
140                        @Nonnull RequestPartitionId theRequestPartitionId, String theResourceType, String theResourceId)
141                        throws ResourceNotFoundException {
142                return resolveResourceIdentity(theRequestPartitionId, theResourceType, theResourceId, false);
143        }
144
145        /**
146         * Given a forced ID, convert it to its Long value. Since you are allowed to use string IDs for resources, we need to
147         * convert those to the underlying Long values that are stored, for lookup and comparison purposes.
148         * Optionally filters out deleted resources.
149         *
150         * @throws ResourceNotFoundException If the ID can not be found
151         */
152        @Override
153        @Nonnull
154        public IResourceLookup<JpaPid> resolveResourceIdentity(
155                        @Nonnull RequestPartitionId theRequestPartitionId,
156                        String theResourceType,
157                        final String theResourceId,
158                        boolean theExcludeDeleted)
159                        throws ResourceNotFoundException {
160                assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive()
161                                : "no transaction active";
162
163                String resourceIdToUse = theResourceId;
164                if (resourceIdToUse.contains("/")) {
165                        resourceIdToUse = theResourceId.substring(resourceIdToUse.indexOf("/") + 1);
166                }
167                IdDt id = new IdDt(theResourceType, resourceIdToUse);
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(resourceIdToUse)) {
173                        throw new ResourceNotFoundException(Msg.code(2001) + "Resource " + id + " is not known");
174                }
175
176                if (matches.size() > 1 || matches.get(resourceIdToUse).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(resourceIdToUse).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(IIdType::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                                        Integer partitionId = (Integer) next[4];
581                                        LocalDate partitionDate = (LocalDate) next[5];
582
583                                        JpaResourceLookup lookup = new JpaResourceLookup(
584                                                        resourceType,
585                                                        resourcePid,
586                                                        deletedAt,
587                                                        PartitionablePartitionId.with(partitionId, partitionDate));
588                                        retVal.computeIfAbsent(forcedId, id -> new ArrayList<>()).add(lookup);
589
590                                        if (!myStorageSettings.isDeleteEnabled()) {
591                                                String key = resourceType + "/" + forcedId;
592                                                myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, key, lookup);
593                                        }
594                                }
595                        }
596                }
597
598                return retVal;
599        }
600
601        public RequestPartitionId replaceDefault(RequestPartitionId theRequestPartitionId) {
602                if (myPartitionSettings.getDefaultPartitionId() != null) {
603                        if (!theRequestPartitionId.isAllPartitions() && theRequestPartitionId.hasDefaultPartitionId()) {
604                                List<Integer> partitionIds = theRequestPartitionId.getPartitionIds().stream()
605                                                .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t)
606                                                .collect(Collectors.toList());
607                                return RequestPartitionId.fromPartitionIds(partitionIds);
608                        }
609                }
610                return theRequestPartitionId;
611        }
612
613        private void resolvePids(
614                        @Nonnull RequestPartitionId theRequestPartitionId,
615                        List<Long> thePidsToResolve,
616                        Map<String, List<IResourceLookup<JpaPid>>> theTargets) {
617                if (!myStorageSettings.isDeleteEnabled()) {
618                        for (Iterator<Long> forcedIdIterator = thePidsToResolve.iterator(); forcedIdIterator.hasNext(); ) {
619                                Long nextPid = forcedIdIterator.next();
620                                String nextKey = Long.toString(nextPid);
621                                IResourceLookup<JpaPid> cachedLookup =
622                                                myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey);
623                                if (cachedLookup != null) {
624                                        forcedIdIterator.remove();
625                                        theTargets.computeIfAbsent(nextKey, id -> new ArrayList<>()).add(cachedLookup);
626                                }
627                        }
628                }
629
630                if (!thePidsToResolve.isEmpty()) {
631                        Collection<Object[]> lookup;
632                        if (theRequestPartitionId.isAllPartitions()) {
633                                lookup = myResourceTableDao.findLookupFieldsByResourcePid(thePidsToResolve);
634                        } else {
635                                if (theRequestPartitionId.isDefaultPartition()) {
636                                        lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionNull(thePidsToResolve);
637                                } else if (theRequestPartitionId.hasDefaultPartitionId()) {
638                                        lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIdsOrNullPartition(
639                                                        thePidsToResolve, theRequestPartitionId.getPartitionIdsWithoutDefault());
640                                } else {
641                                        lookup = myResourceTableDao.findLookupFieldsByResourcePidInPartitionIds(
642                                                        thePidsToResolve, theRequestPartitionId.getPartitionIds());
643                                }
644                        }
645                        lookup.stream()
646                                        .map(t -> new JpaResourceLookup(
647                                                        (String) t[0],
648                                                        (Long) t[1],
649                                                        (Date) t[2],
650                                                        PartitionablePartitionId.with((Integer) t[3], (LocalDate) t[4])))
651                                        .forEach(t -> {
652                                                String id = t.getPersistentId().toString();
653                                                if (!theTargets.containsKey(id)) {
654                                                        theTargets.put(id, new ArrayList<>());
655                                                }
656                                                theTargets.get(id).add(t);
657                                                if (!myStorageSettings.isDeleteEnabled()) {
658                                                        String nextKey = t.getPersistentId().toString();
659                                                        myMemoryCacheService.putAfterCommit(
660                                                                        MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, t);
661                                                }
662                                        });
663                }
664        }
665
666        @Override
667        public PersistentIdToForcedIdMap<JpaPid> translatePidsToForcedIds(Set<JpaPid> theResourceIds) {
668                assert myDontCheckActiveTransactionForUnitTest || TransactionSynchronizationManager.isSynchronizationActive();
669                Set<Long> thePids = theResourceIds.stream().map(JpaPid::getId).collect(Collectors.toSet());
670                Map<Long, Optional<String>> retVal = new HashMap<>(
671                                myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, thePids));
672
673                List<Long> remainingPids =
674                                thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList());
675
676                new QueryChunker<Long>().chunk(remainingPids, t -> {
677                        List<ResourceTable> resourceEntities = myResourceTableDao.findAllById(t);
678
679                        for (ResourceTable nextResourceEntity : resourceEntities) {
680                                Long nextResourcePid = nextResourceEntity.getId();
681                                Optional<String> nextForcedId = Optional.of(nextResourceEntity.asTypedFhirResourceId());
682                                retVal.put(nextResourcePid, nextForcedId);
683                                myMemoryCacheService.putAfterCommit(
684                                                MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, nextForcedId);
685                        }
686                });
687
688                remainingPids = thePids.stream().filter(t -> !retVal.containsKey(t)).collect(Collectors.toList());
689                for (Long nextResourcePid : remainingPids) {
690                        retVal.put(nextResourcePid, Optional.empty());
691                        myMemoryCacheService.putAfterCommit(
692                                        MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, nextResourcePid, Optional.empty());
693                }
694                Map<JpaPid, Optional<String>> convertRetVal = new HashMap<>();
695                retVal.forEach((k, v) -> convertRetVal.put(JpaPid.fromId(k), v));
696
697                return new PersistentIdToForcedIdMap<>(convertRetVal);
698        }
699
700        /**
701         * Pre-cache a PID-to-Resource-ID mapping for later retrieval by {@link #translatePidsToForcedIds(Set)} and related methods
702         */
703        @Override
704        public void addResolvedPidToForcedId(
705                        JpaPid theJpaPid,
706                        @Nonnull RequestPartitionId theRequestPartitionId,
707                        String theResourceType,
708                        @Nullable String theForcedId,
709                        @Nullable Date theDeletedAt) {
710                if (theForcedId != null) {
711                        if (theJpaPid.getAssociatedResourceId() == null) {
712                                populateAssociatedResourceId(theResourceType, theForcedId, theJpaPid);
713                        }
714
715                        myMemoryCacheService.putAfterCommit(
716                                        MemoryCacheService.CacheEnum.PID_TO_FORCED_ID,
717                                        theJpaPid.getId(),
718                                        Optional.of(theResourceType + "/" + theForcedId));
719                        String key = toForcedIdToPidKey(theRequestPartitionId, theResourceType, theForcedId);
720                        myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.FORCED_ID_TO_PID, key, theJpaPid);
721                } else {
722                        myMemoryCacheService.putAfterCommit(
723                                        MemoryCacheService.CacheEnum.PID_TO_FORCED_ID, theJpaPid.getId(), Optional.empty());
724                }
725
726                if (!myStorageSettings.isDeleteEnabled()) {
727                        JpaResourceLookup lookup = new JpaResourceLookup(
728                                        theResourceType, theJpaPid.getId(), theDeletedAt, theJpaPid.getPartitionablePartitionId());
729                        String nextKey = theJpaPid.toString();
730                        myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_LOOKUP, nextKey, lookup);
731                }
732        }
733
734        @VisibleForTesting
735        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
736                myPartitionSettings = thePartitionSettings;
737        }
738
739        public static boolean isValidPid(IIdType theId) {
740                if (theId == null) {
741                        return false;
742                }
743
744                String idPart = theId.getIdPart();
745                return isValidPid(idPart);
746        }
747
748        public static boolean isValidPid(String theIdPart) {
749                return StringUtils.isNumeric(theIdPart);
750        }
751
752        @Override
753        @Nonnull
754        public List<JpaPid> getPidsOrThrowException(
755                        @Nonnull RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
756                return resolveResourcePersistentIdsWithCache(theRequestPartitionId, theIds);
757        }
758
759        @Override
760        @Nullable
761        public JpaPid getPidOrNull(@Nonnull RequestPartitionId theRequestPartitionId, IBaseResource theResource) {
762                Object resourceId = theResource.getUserData(RESOURCE_PID);
763                JpaPid retVal;
764                if (resourceId == null) {
765                        IIdType id = theResource.getIdElement();
766                        try {
767                                retVal = resolveResourcePersistentIds(theRequestPartitionId, id.getResourceType(), id.getIdPart());
768                        } catch (ResourceNotFoundException e) {
769                                retVal = null;
770                        }
771                } else {
772                        retVal = JpaPid.fromId(Long.parseLong(resourceId.toString()));
773                }
774                return retVal;
775        }
776
777        @Override
778        @Nonnull
779        public JpaPid getPidOrThrowException(@Nonnull RequestPartitionId theRequestPartitionId, IIdType theId) {
780                List<IIdType> ids = Collections.singletonList(theId);
781                List<JpaPid> resourcePersistentIds = resolveResourcePersistentIdsWithCache(theRequestPartitionId, ids);
782                if (resourcePersistentIds.isEmpty()) {
783                        throw new InvalidRequestException(Msg.code(2295) + "Invalid ID was provided: [" + theId.getIdPart() + "]");
784                }
785                return resourcePersistentIds.get(0);
786        }
787
788        @Override
789        @Nonnull
790        public JpaPid getPidOrThrowException(@Nonnull IAnyResource theResource) {
791                Long theResourcePID = (Long) theResource.getUserData(RESOURCE_PID);
792                if (theResourcePID == null) {
793                        throw new IllegalStateException(Msg.code(2108)
794                                        + String.format(
795                                                        "Unable to find %s in the user data for %s with ID %s",
796                                                        RESOURCE_PID, theResource, theResource.getId()));
797                }
798                return JpaPid.fromId(theResourcePID);
799        }
800
801        @Override
802        public IIdType resourceIdFromPidOrThrowException(JpaPid thePid, String theResourceType) {
803                Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid.getId());
804                if (optionalResource.isEmpty()) {
805                        throw new ResourceNotFoundException(Msg.code(2124) + "Requested resource not found");
806                }
807                return optionalResource.get().getIdDt().toVersionless();
808        }
809
810        /**
811         * Given a set of PIDs, return a set of public FHIR Resource IDs.
812         * 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
813         * Example:
814         * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows:
815         * <p>
816         * [1,2,3] -> ["1","pat1","3"]
817         *
818         * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs.
819         * @return A Set of strings representing the FHIR IDs of the pids.
820         */
821        @Override
822        public Set<String> translatePidsToFhirResourceIds(Set<JpaPid> thePids) {
823                assert TransactionSynchronizationManager.isSynchronizationActive();
824
825                PersistentIdToForcedIdMap<JpaPid> pidToForcedIdMap = translatePidsToForcedIds(thePids);
826
827                return pidToForcedIdMap.getResolvedResourceIds();
828        }
829
830        @Override
831        public JpaPid newPid(Object thePid) {
832                return JpaPid.fromId((Long) thePid);
833        }
834
835        @Override
836        public JpaPid newPidFromStringIdAndResourceName(String thePid, String theResourceName) {
837                return JpaPid.fromIdAndResourceType(Long.parseLong(thePid), theResourceName);
838        }
839}