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.partition.IRequestPartitionHelperSvc;
031import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
032import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
033import jakarta.annotation.Nonnull;
034import org.hl7.fhir.instance.model.api.IIdType;
035import org.slf4j.Logger;
036import org.springframework.beans.factory.annotation.Autowired;
037import org.springframework.stereotype.Service;
038
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.HashMap;
042import java.util.List;
043import java.util.Map;
044
045import static org.slf4j.LoggerFactory.getLogger;
046
047/**
048 * This service builds a map of resource ids to versions based on a SearchParameterMap.
049 * It is used by the in-memory resource-version cache to detect when resource versions have been changed by remote processes.
050 */
051@Service
052public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
053        private static final Logger ourLog = getLogger(ResourceVersionSvcDaoImpl.class);
054
055        @Autowired
056        DaoRegistry myDaoRegistry;
057
058        @Autowired
059        IResourceTableDao myResourceTableDao;
060
061        @Autowired
062        IIdHelperService<JpaPid> myIdHelperService;
063
064        @Autowired
065        IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
066
067        @Override
068        @Nonnull
069        public ResourceVersionMap getVersionMap(
070                        RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
071                if (ourLog.isDebugEnabled()) {
072                        ourLog.debug("About to retrieve version map for resource type: {}", theResourceName);
073                }
074
075                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
076                SystemRequestDetails request = new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId);
077
078                List<IIdType> fhirIds = dao.searchForResourceIds(theSearchParamMap, request);
079
080                return ResourceVersionMap.fromIdsWithVersions(fhirIds);
081        }
082
083        /**
084         * Retrieves the latest versions for any resourceid that are found.
085         * If they are not found, they will not be contained in the returned map.
086         * The key should be the same value that was passed in to allow
087         * consumer to look up the value using the id they already have.
088         *
089         * This method should not throw, so it can safely be consumed in
090         * transactions.
091         *
092         * @param theRequestPartitionId - request partition id
093         * @param theIds - list of IIdTypes for resources of interest.
094         */
095        @Override
096        public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(
097                        RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
098                ResourcePersistentIdMap idToPID = new ResourcePersistentIdMap();
099
100                ResourcePersistentIdMap idAndPID = getIdsOfExistingResources(theRequestPartitionId, theIds);
101                idToPID.putAll(idAndPID);
102
103                return idToPID;
104        }
105
106        /**
107         * Helper method to determine if some resources exist in the DB (without throwing).
108         * Returns a set that contains the IIdType for every resource found.
109         * If it's not found, it won't be included in the set.
110         *
111         * @param theIds - list of IIdType ids (for the same resource)
112         */
113        private ResourcePersistentIdMap getIdsOfExistingResources(
114                        RequestPartitionId thePartitionId, Collection<IIdType> theIds) {
115                // these are the found Ids that were in the db
116                ResourcePersistentIdMap retVal = new ResourcePersistentIdMap();
117
118                if (theIds == null || theIds.isEmpty()) {
119                        return retVal;
120                }
121
122                Map<IIdType, IResourceLookup<JpaPid>> identities = myIdHelperService.resolveResourceIdentities(
123                                thePartitionId,
124                                new ArrayList<>(theIds),
125                                ResolveIdentityMode.includeDeleted().cacheOk());
126
127                Map<String, JpaPid> entriesWithoutVersion = new HashMap<>(identities.size());
128
129                for (Map.Entry<IIdType, IResourceLookup<JpaPid>> entry : identities.entrySet()) {
130                        IResourceLookup<JpaPid> lookup = entry.getValue();
131                        JpaPid persistentId = lookup.getPersistentId();
132                        retVal.put(entry.getKey(), persistentId);
133                        if (persistentId.getVersion() == null) {
134                                entriesWithoutVersion.put(
135                                                entry.getKey().toUnqualifiedVersionless().getValue(), persistentId);
136                        }
137                }
138
139                // set any versions we don't already have
140                if (!entriesWithoutVersion.isEmpty()) {
141                        Collection<Object[]> resourceEntries =
142                                        myResourceTableDao.getResourceVersionsForPid(entriesWithoutVersion.values());
143
144                        for (Object[] nextRecord : resourceEntries) {
145                                // order matters!
146                                JpaPid retPid = (JpaPid) nextRecord[0];
147                                String resType = (String) nextRecord[1];
148                                String fhirId = (String) nextRecord[2];
149                                Long version = (Long) nextRecord[3];
150                                JpaPid jpaPid = entriesWithoutVersion.get(resType + "/" + fhirId);
151                                if (jpaPid != null) {
152                                        jpaPid.setVersion(version);
153                                }
154                        }
155                }
156
157                return retVal;
158        }
159}