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.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.dao.data.IResourceTableDao;
027import ca.uhn.fhir.jpa.model.dao.JpaPid;
028import ca.uhn.fhir.jpa.model.entity.ResourceTable;
029import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
030import ca.uhn.fhir.jpa.util.QueryChunker;
031import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
032import org.hl7.fhir.instance.model.api.IIdType;
033import org.slf4j.Logger;
034import org.springframework.beans.factory.annotation.Autowired;
035import org.springframework.stereotype.Service;
036import org.springframework.transaction.annotation.Transactional;
037
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Optional;
043import java.util.stream.Collectors;
044import javax.annotation.Nonnull;
045
046import static org.slf4j.LoggerFactory.getLogger;
047
048/**
049 * This service builds a map of resource ids to versions based on a SearchParameterMap.
050 * It is used by the in-memory resource-version cache to detect when resource versions have been changed by remote processes.
051 */
052@Service
053public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
054        private static final Logger ourLog = getLogger(ResourceVersionSvcDaoImpl.class);
055
056        @Autowired
057        DaoRegistry myDaoRegistry;
058
059        @Autowired
060        IResourceTableDao myResourceTableDao;
061
062        @Autowired
063        IIdHelperService<JpaPid> myIdHelperService;
064
065        @Override
066        @Nonnull
067        @Transactional
068        public ResourceVersionMap getVersionMap(
069                        RequestPartitionId theRequestPartitionId, String theResourceName, SearchParameterMap theSearchParamMap) {
070                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
071
072                if (ourLog.isDebugEnabled()) {
073                        ourLog.debug("About to retrieve version map for resource type: {}", theResourceName);
074                }
075
076                List<JpaPid> jpaPids = dao.searchForIds(
077                                theSearchParamMap, new SystemRequestDetails().setRequestPartitionId(theRequestPartitionId));
078                List<Long> matchingIds = jpaPids.stream().map(JpaPid::getId).collect(Collectors.toList());
079
080                List<ResourceTable> allById = new ArrayList<>();
081                new QueryChunker<Long>().chunk(matchingIds, t -> {
082                        List<ResourceTable> nextBatch = myResourceTableDao.findAllById(t);
083                        allById.addAll(nextBatch);
084                });
085
086                return ResourceVersionMap.fromResourceTableEntities(allById);
087        }
088
089        @Override
090        /**
091         * Retrieves the latest versions for any resourceid that are found.
092         * If they are not found, they will not be contained in the returned map.
093         * The key should be the same value that was passed in to allow
094         * consumer to look up the value using the id they already have.
095         *
096         * This method should not throw, so it can safely be consumed in
097         * transactions.
098         *
099         * @param theRequestPartitionId - request partition id
100         * @param theIds - list of IIdTypes for resources of interest.
101         * @return
102         */
103        public ResourcePersistentIdMap getLatestVersionIdsForResourceIds(
104                        RequestPartitionId theRequestPartitionId, List<IIdType> theIds) {
105                ResourcePersistentIdMap idToPID = new ResourcePersistentIdMap();
106                HashMap<String, List<IIdType>> resourceTypeToIds = new HashMap<>();
107
108                for (IIdType id : theIds) {
109                        String resourceType = id.getResourceType();
110                        if (!resourceTypeToIds.containsKey(resourceType)) {
111                                resourceTypeToIds.put(resourceType, new ArrayList<>());
112                        }
113                        resourceTypeToIds.get(resourceType).add(id);
114                }
115
116                for (String resourceType : resourceTypeToIds.keySet()) {
117                        ResourcePersistentIdMap idAndPID =
118                                        getIdsOfExistingResources(theRequestPartitionId, resourceTypeToIds.get(resourceType));
119                        idToPID.putAll(idAndPID);
120                }
121
122                return idToPID;
123        }
124
125        /**
126         * Helper method to determine if some resources exist in the DB (without throwing).
127         * Returns a set that contains the IIdType for every resource found.
128         * If it's not found, it won't be included in the set.
129         *
130         * @param theIds - list of IIdType ids (for the same resource)
131         * @return
132         */
133        private ResourcePersistentIdMap getIdsOfExistingResources(
134                        RequestPartitionId thePartitionId, Collection<IIdType> theIds) {
135                // these are the found Ids that were in the db
136                ResourcePersistentIdMap retval = new ResourcePersistentIdMap();
137
138                if (theIds == null || theIds.isEmpty()) {
139                        return retval;
140                }
141
142                List<JpaPid> jpaPids =
143                                myIdHelperService.resolveResourcePersistentIdsWithCache(thePartitionId, new ArrayList<>(theIds));
144
145                // we'll use this map to fetch pids that require versions
146                HashMap<Long, JpaPid> pidsToVersionToResourcePid = new HashMap<>();
147
148                // fill in our map
149                for (JpaPid pid : jpaPids) {
150                        if (pid.getVersion() == null) {
151                                pidsToVersionToResourcePid.put(pid.getId(), pid);
152                        }
153                        Optional<IIdType> idOp = theIds.stream()
154                                        .filter(i ->
155                                                        i.getIdPart().equals(pid.getAssociatedResourceId().getIdPart()))
156                                        .findFirst();
157                        // this should always be present
158                        // since it was passed in.
159                        // but land of optionals...
160                        idOp.ifPresent(id -> {
161                                retval.put(id, pid);
162                        });
163                }
164
165                // set any versions we don't already have
166                if (!pidsToVersionToResourcePid.isEmpty()) {
167                        Collection<Object[]> resourceEntries =
168                                        myResourceTableDao.getResourceVersionsForPid(new ArrayList<>(pidsToVersionToResourcePid.keySet()));
169
170                        for (Object[] record : resourceEntries) {
171                                // order matters!
172                                Long retPid = (Long) record[0];
173                                String resType = (String) record[1];
174                                Long version = (Long) record[2];
175                                pidsToVersionToResourcePid.get(retPid).setVersion(version);
176                        }
177                }
178
179                return retval;
180        }
181}