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