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.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;
043import java.util.Optional;
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        @Override
065        @Nonnull
066        public ResourceVersionMap getVersionMap(
067                        RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
068                if (ourLog.isDebugEnabled()) {
069                        ourLog.debug("About to retrieve version map for resource type: {}", theResourceName);
070                }
071
072                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
073                SystemRequestDetails request = new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId);
074
075                List<IIdType> fhirIds = dao.searchForResourceIds(theSearchParamMap, request);
076
077                return ResourceVersionMap.fromIdsWithVersions(fhirIds);
078        }
079
080        /**
081         * Retrieves the latest versions for any resourceid that are found.
082         * If they are not found, they will not be contained in the returned map.
083         * The key should be the same value that was passed in to allow
084         * consumer to look up the value using the id they already have.
085         *
086         * This method should not throw, so it can safely be consumed in
087         * transactions.
088         *
089         * @param theRequestPartitionId - request partition id
090         * @param theIds - list of IIdTypes for resources of interest.
091         */
092        @Override
093        public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(
094                        RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
095                ResourcePersistentIdMap idToPID = new ResourcePersistentIdMap();
096
097                ResourcePersistentIdMap idAndPID = getIdsOfExistingResources(theRequestPartitionId, theIds);
098                idToPID.putAll(idAndPID);
099
100                return idToPID;
101        }
102
103        /**
104         * Helper method to determine if some resources exist in the DB (without throwing).
105         * Returns a set that contains the IIdType for every resource found.
106         * If it's not found, it won't be included in the set.
107         *
108         * @param theIds - list of IIdType ids (for the same resource)
109         */
110        private ResourcePersistentIdMap getIdsOfExistingResources(
111                        RequestPartitionId thePartitionId, Collection<IIdType> theIds) {
112                // these are the found Ids that were in the db
113                ResourcePersistentIdMap retval = new ResourcePersistentIdMap();
114
115                if (theIds == null || theIds.isEmpty()) {
116                        return retval;
117                }
118
119                Map<IIdType, IResourceLookup<JpaPid>> identities = myIdHelperService.resolveResourceIdentities(
120                                thePartitionId,
121                                new ArrayList<>(theIds),
122                                ResolveIdentityMode.includeDeleted().cacheOk());
123
124                // we'll use this map to fetch pids that require versions
125                HashMap<Long, JpaPid> pidsToVersionToResourcePid = new HashMap<>();
126
127                // fill in our map
128                for (IResourceLookup<JpaPid> identity : identities.values()) {
129                        JpaPid pid = identity.getPersistentId();
130                        if (pid.getVersion() == null) {
131                                pidsToVersionToResourcePid.put(pid.getId(), pid);
132                        }
133                        Optional<IIdType> idOp = theIds.stream()
134                                        .filter(i ->
135                                                        i.getIdPart().equals(pid.getAssociatedResourceId().getIdPart()))
136                                        .findFirst();
137                        // this should always be present
138                        // since it was passed in.
139                        // but land of optionals...
140                        idOp.ifPresent(id -> retval.put(id, pid));
141                }
142
143                // set any versions we don't already have
144                if (!pidsToVersionToResourcePid.isEmpty()) {
145                        Collection<Object[]> resourceEntries =
146                                        myResourceTableDao.getResourceVersionsForPid(new ArrayList<>(pidsToVersionToResourcePid.keySet()));
147
148                        for (Object[] nextRecord : resourceEntries) {
149                                // order matters!
150                                Long retPid = (Long) nextRecord[0];
151                                String resType = (String) nextRecord[1];
152                                Long version = (Long) nextRecord[2];
153                                pidsToVersionToResourcePid.get(retPid).setVersion(version);
154                        }
155                }
156
157                return retval;
158        }
159}