001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.cache;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
025import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
026import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
027import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
028import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
029import ca.uhn.fhir.jpa.model.dao.JpaPid;
030import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
031import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
032import jakarta.annotation.Nonnull;
033import org.hl7.fhir.instance.model.api.IIdType;
034import org.slf4j.Logger;
035import org.springframework.beans.factory.annotation.Autowired;
036import org.springframework.stereotype.Service;
037
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043
044import static org.slf4j.LoggerFactory.getLogger;
045
046/**
047 * This service builds a map of resource ids to versions based on a SearchParameterMap.
048 * It is used by the in-memory resource-version cache to detect when resource versions have been changed by remote processes.
049 */
050@Service
051public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
052        private static final Logger ourLog = getLogger(ResourceVersionSvcDaoImpl.class);
053
054        @Autowired
055        DaoRegistry myDaoRegistry;
056
057        @Autowired
058        IResourceTableDao myResourceTableDao;
059
060        @Autowired
061        IIdHelperService<JpaPid> myIdHelperService;
062
063        @Override
064        @Nonnull
065        public ResourceVersionMap getVersionMap(
066                        RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
067                if (ourLog.isDebugEnabled()) {
068                        ourLog.debug("About to retrieve version map for resource type: {}", theResourceName);
069                }
070
071                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
072                SystemRequestDetails request = new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId);
073
074                List<IIdType> fhirIds = dao.searchForResourceIds(theSearchParamMap, request);
075
076                return ResourceVersionMap.fromIdsWithVersions(fhirIds);
077        }
078
079        /**
080         * Retrieves the latest versions for any resourceid that are found.
081         * If they are not found, they will not be contained in the returned map.
082         * The key should be the same value that was passed in to allow
083         * consumer to look up the value using the id they already have.
084         *
085         * This method should not throw, so it can safely be consumed in
086         * transactions.
087         *
088         * @param theRequestPartitionId - request partition id
089         * @param theIds - list of IIdTypes for resources of interest.
090         */
091        @Override
092        public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(
093                        RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
094                ResourcePersistentIdMap idToPID = new ResourcePersistentIdMap();
095
096                ResourcePersistentIdMap idAndPID = getIdsOfExistingResources(theRequestPartitionId, theIds);
097                idToPID.putAll(idAndPID);
098
099                return idToPID;
100        }
101
102        /**
103         * Helper method to determine if some resources exist in the DB (without throwing).
104         * Returns a set that contains the IIdType for every resource found.
105         * If it's not found, it won't be included in the set.
106         *
107         * @param theIds - list of IIdType ids (for the same resource)
108         */
109        private ResourcePersistentIdMap getIdsOfExistingResources(
110                        RequestPartitionId thePartitionId, Collection<IIdType> theIds) {
111                // these are the found Ids that were in the db
112                ResourcePersistentIdMap retVal = new ResourcePersistentIdMap();
113
114                if (theIds == null || theIds.isEmpty()) {
115                        return retVal;
116                }
117
118                Map<IIdType, IResourceLookup<JpaPid>> identities = myIdHelperService.resolveResourceIdentities(
119                                thePartitionId,
120                                new ArrayList<>(theIds),
121                                ResolveIdentityMode.includeDeleted().cacheOk());
122
123                Map<String, JpaPid> entriesWithoutVersion = new HashMap<>(identities.size());
124
125                for (Map.Entry<IIdType, IResourceLookup<JpaPid>> entry : identities.entrySet()) {
126                        IResourceLookup<JpaPid> lookup = entry.getValue();
127                        JpaPid persistentId = lookup.getPersistentId();
128                        retVal.put(entry.getKey(), persistentId);
129                        if (persistentId.getVersion() == null) {
130                                entriesWithoutVersion.put(
131                                                entry.getKey().toUnqualifiedVersionless().getValue(), persistentId);
132                        }
133                }
134
135                // set any versions we don't already have
136                if (!entriesWithoutVersion.isEmpty()) {
137                        Collection<Object[]> resourceEntries =
138                                        myResourceTableDao.getResourceVersionsForPid(entriesWithoutVersion.values());
139
140                        for (Object[] nextRecord : resourceEntries) {
141                                // order matters!
142                                JpaPid retPid = (JpaPid) nextRecord[0];
143                                String resType = (String) nextRecord[1];
144                                String fhirId = (String) nextRecord[2];
145                                Long version = (Long) nextRecord[3];
146                                JpaPid jpaPid = entriesWithoutVersion.get(resType + "/" + fhirId);
147                                if (jpaPid != null) {
148                                        jpaPid.setVersion(version);
149                                }
150                        }
151                }
152
153                return retVal;
154        }
155}