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.batch2;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
027import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
028import ca.uhn.fhir.jpa.api.pid.IResourcePidList;
029import ca.uhn.fhir.jpa.api.pid.IResourcePidStream;
030import ca.uhn.fhir.jpa.api.pid.StreamTemplate;
031import ca.uhn.fhir.jpa.api.pid.TypedResourcePid;
032import ca.uhn.fhir.jpa.api.pid.TypedResourceStream;
033import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc;
034import ca.uhn.fhir.jpa.dao.ISearchBuilder;
035import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
036import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
037import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
038import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
039import ca.uhn.fhir.jpa.model.config.PartitionSettings;
040import ca.uhn.fhir.jpa.model.dao.JpaPid;
041import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
042import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
043import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
044import ca.uhn.fhir.model.primitive.IdDt;
045import ca.uhn.fhir.rest.api.Constants;
046import ca.uhn.fhir.rest.api.SortSpec;
047import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
048import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
049import ca.uhn.fhir.util.DateRangeUtil;
050import ca.uhn.fhir.util.Logs;
051import ca.uhn.fhir.util.UrlUtil;
052import jakarta.annotation.Nonnull;
053import jakarta.annotation.Nullable;
054import org.apache.commons.lang3.StringUtils;
055import org.apache.commons.lang3.Validate;
056import org.hl7.fhir.instance.model.api.IIdType;
057
058import java.util.Date;
059import java.util.UUID;
060import java.util.function.Supplier;
061import java.util.stream.Stream;
062
063import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
064
065public class Batch2DaoSvcImpl implements IBatch2DaoSvc {
066        private static final org.slf4j.Logger ourLog = Logs.getBatchTroubleshootingLog();
067
068        private final IResourceTableDao myResourceTableDao;
069
070        private final IResourceLinkDao myResourceLinkDao;
071
072        private final MatchUrlService myMatchUrlService;
073
074        private final DaoRegistry myDaoRegistry;
075
076        private final FhirContext myFhirContext;
077
078        private final IHapiTransactionService myTransactionService;
079
080        private final PartitionSettings myPartitionSettings;
081
082        private final SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
083
084        @Override
085        public boolean isAllResourceTypeSupported() {
086                return true;
087        }
088
089        public Batch2DaoSvcImpl(
090                        IResourceTableDao theResourceTableDao,
091                        IResourceLinkDao theResourceLinkDao,
092                        MatchUrlService theMatchUrlService,
093                        DaoRegistry theDaoRegistry,
094                        FhirContext theFhirContext,
095                        IHapiTransactionService theTransactionService,
096                        PartitionSettings thePartitionSettings,
097                        SearchBuilderFactory<JpaPid> theSearchBuilderFactory) {
098                myResourceTableDao = theResourceTableDao;
099                myResourceLinkDao = theResourceLinkDao;
100                myMatchUrlService = theMatchUrlService;
101                myDaoRegistry = theDaoRegistry;
102                myFhirContext = theFhirContext;
103                myTransactionService = theTransactionService;
104                myPartitionSettings = thePartitionSettings;
105                mySearchBuilderFactory = theSearchBuilderFactory;
106        }
107
108        @Override
109        public IResourcePidStream fetchResourceIdStream(
110                        Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId, String theUrl) {
111
112                if (StringUtils.isBlank(theUrl)) {
113                        // first scenario
114                        return makeStreamResult(
115                                        theRequestPartitionId, () -> streamResourceIdsNoUrl(theStart, theEnd, theRequestPartitionId));
116                } else {
117                        return makeStreamResult(
118                                        theRequestPartitionId,
119                                        () -> streamResourceIdsWithUrl(theStart, theEnd, theUrl, theRequestPartitionId));
120                }
121        }
122
123        @Override
124        public Stream<IdDt> streamSourceIdsThatReferenceTargetId(IIdType theTargetId) {
125                return myResourceLinkDao.streamSourceIdsForTargetFhirId(theTargetId.getResourceType(), theTargetId.getIdPart());
126        }
127
128        private Stream<TypedResourcePid> streamResourceIdsWithUrl(
129                        Date theStart, Date theEnd, String theUrl, RequestPartitionId theRequestPartitionId) {
130                validateUrl(theUrl);
131
132                String resourceType = UrlUtil.determineResourceTypeInResourceUrl(myFhirContext, theUrl);
133
134                // Search in all partitions if no partition is provided
135                ourLog.debug("No partition id detected in request - searching all partitions");
136                RequestPartitionId thePartitionId = defaultIfNull(theRequestPartitionId, RequestPartitionId.allPartitions());
137
138                SearchParameterMap searchParamMap;
139                SystemRequestDetails request = new SystemRequestDetails();
140                request.setRequestPartitionId(thePartitionId);
141
142                if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) {
143                        searchParamMap = parseQuery(theUrl, null);
144                } else {
145                        searchParamMap = parseQuery(theUrl);
146                }
147
148                searchParamMap.setLastUpdated(DateRangeUtil.narrowDateRange(searchParamMap.getLastUpdated(), theStart, theEnd));
149
150                if (isNoResourceTypeProvidedInUrl(theUrl, resourceType)) {
151                        return searchForResourceIdsAndType(thePartitionId, request, searchParamMap);
152                }
153
154                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
155
156                return dao.searchForIdStream(searchParamMap, request, null).map(pid -> new TypedResourcePid(resourceType, pid));
157        }
158
159        /**
160         * Since the resource type is not specified, query the DB for resources matching the params and return resource ID and type
161         *
162         * @param theRequestPartitionId the partition to search on
163         * @param theRequestDetails the theRequestDetails details
164         * @param theSearchParams the search params
165         * @return Stream of typed resource pids
166         */
167        private Stream<TypedResourcePid> searchForResourceIdsAndType(
168                        RequestPartitionId theRequestPartitionId,
169                        SystemRequestDetails theRequestDetails,
170                        SearchParameterMap theSearchParams) {
171                ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(null, null);
172                return myTransactionService
173                                .withRequest(theRequestDetails)
174                                .withRequestPartitionId(theRequestPartitionId)
175                                .search(partition -> builder.createQueryStream(
176                                                theSearchParams,
177                                                new SearchRuntimeDetails(
178                                                                theRequestDetails, UUID.randomUUID().toString()),
179                                                theRequestDetails,
180                                                partition))
181                                .map(pid -> new TypedResourcePid(pid.getResourceType(), pid));
182        }
183
184        @Nonnull
185        private TypedResourceStream makeStreamResult(
186                        RequestPartitionId theRequestPartitionId, Supplier<Stream<TypedResourcePid>> streamSupplier) {
187
188                IHapiTransactionService.IExecutionBuilder txSettings =
189                                myTransactionService.withSystemRequest().withRequestPartitionId(theRequestPartitionId);
190
191                StreamTemplate<TypedResourcePid> streamTemplate =
192                                StreamTemplate.fromSupplier(streamSupplier).withTransactionAdvice(txSettings);
193
194                return new TypedResourceStream(theRequestPartitionId, streamTemplate);
195        }
196
197        /**
198         * At the moment there is no use-case for this method.
199         * This can be cleaned up at a later point in time if there is no use for it.
200         */
201        @Nonnull
202        private Stream<TypedResourcePid> streamResourceIdsNoUrl(
203                        Date theStart, Date theEnd, RequestPartitionId theRequestPartitionId) {
204                Integer defaultPartitionId = myPartitionSettings.getDefaultPartitionId();
205                Stream<Object[]> rowStream;
206
207                if (theRequestPartitionId == null || theRequestPartitionId.isAllPartitions()) {
208                        ourLog.debug("Search for resources - all partitions");
209                        rowStream = myResourceTableDao.streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldest(
210                                        theStart, theEnd);
211                } else if (theRequestPartitionId.isPartition(defaultPartitionId)) {
212                        ourLog.debug("Search for resources - default partition");
213                        rowStream =
214                                        myResourceTableDao
215                                                        .streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldestForDefaultPartition(
216                                                                        theStart, theEnd, defaultPartitionId);
217                } else {
218                        ourLog.debug("Search for resources - partition {}", theRequestPartitionId);
219                        rowStream =
220                                        myResourceTableDao
221                                                        .streamIdsTypesAndUpdateTimesOfResourcesWithinUpdatedRangeOrderedFromOldestForPartitionIds(
222                                                                        theStart, theEnd, theRequestPartitionId.getPartitionIds());
223                }
224
225                return rowStream.map(Batch2DaoSvcImpl::typedPidFromQueryArray);
226        }
227
228        @Deprecated(since = "6.11", forRemoval = true) // delete once the default method in the interface is gone.
229        @Override
230        public IResourcePidList fetchResourceIdsPage(
231                        Date theStart, Date theEnd, @Nullable RequestPartitionId theRequestPartitionId, @Nullable String theUrl) {
232                Validate.isTrue(false, "Unimplemented");
233                return null;
234        }
235
236        @Nonnull
237        private SearchParameterMap parseQuery(String theUrl) {
238                String resourceType = theUrl.substring(0, theUrl.indexOf('?'));
239                RuntimeResourceDefinition def = myFhirContext.getResourceDefinition(resourceType);
240
241                return parseQuery(theUrl, def);
242        }
243
244        @Nonnull
245        private SearchParameterMap parseQuery(
246                        String theUrl, @Nullable RuntimeResourceDefinition theRuntimeResourceDefinition) {
247                SearchParameterMap searchParamMap = myMatchUrlService.translateMatchUrl(theUrl, theRuntimeResourceDefinition);
248                // this matches idx_res_type_del_updated
249                searchParamMap.setSort(new SortSpec(Constants.PARAM_LASTUPDATED).setChain(new SortSpec(Constants.PARAM_PID)));
250                // TODO this limits us to 2G resources.
251                searchParamMap.setLoadSynchronousUpTo(Integer.MAX_VALUE);
252                return searchParamMap;
253        }
254
255        private static TypedResourcePid typedPidFromQueryArray(Object[] thePidTypeDateArray) {
256                JpaPid pid = (JpaPid) thePidTypeDateArray[0];
257                String resourceType = (String) thePidTypeDateArray[1];
258                return new TypedResourcePid(resourceType, pid);
259        }
260
261        private static void validateUrl(@Nonnull String theUrl) {
262                if (!theUrl.contains("?")) {
263                        throw new InternalErrorException(Msg.code(2422) + "this should never happen: URL is missing a '?'");
264                }
265        }
266
267        private static boolean isNoResourceTypeProvidedInUrl(String theUrl, String resourceType) {
268                return (resourceType == null || resourceType.isBlank()) && theUrl.indexOf('?') == 0;
269        }
270}