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.search;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.interceptor.model.RequestPartitionId;
028import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
029import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
030import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
031import ca.uhn.fhir.jpa.dao.IResultIterator;
032import ca.uhn.fhir.jpa.dao.ISearchBuilder;
033import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
034import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
035import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
036import ca.uhn.fhir.jpa.model.dao.JpaPid;
037import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
038import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
039import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
040import ca.uhn.fhir.model.api.IQueryParameterType;
041import ca.uhn.fhir.rest.api.Constants;
042import ca.uhn.fhir.rest.api.server.IBundleProvider;
043import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
044import ca.uhn.fhir.rest.api.server.RequestDetails;
045import ca.uhn.fhir.rest.server.SimpleBundleProvider;
046import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
047import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil;
048import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
049import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
050import jakarta.persistence.EntityManager;
051import org.hl7.fhir.instance.model.api.IBaseResource;
052import org.springframework.beans.factory.annotation.Autowired;
053
054import java.io.IOException;
055import java.util.ArrayList;
056import java.util.List;
057import java.util.Set;
058import java.util.UUID;
059
060import static ca.uhn.fhir.jpa.util.SearchParameterMapCalculator.isWantCount;
061import static ca.uhn.fhir.jpa.util.SearchParameterMapCalculator.isWantOnlyCount;
062import static java.util.Objects.nonNull;
063
064public class SynchronousSearchSvcImpl implements ISynchronousSearchSvc {
065
066        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SynchronousSearchSvcImpl.class);
067
068        private FhirContext myContext;
069
070        @Autowired
071        private JpaStorageSettings myStorageSettings;
072
073        @Autowired
074        protected SearchBuilderFactory mySearchBuilderFactory;
075
076        @Autowired
077        private DaoRegistry myDaoRegistry;
078
079        @Autowired
080        private HapiTransactionService myTxService;
081
082        @Autowired
083        private IInterceptorBroadcaster myInterceptorBroadcaster;
084
085        @Autowired
086        private EntityManager myEntityManager;
087
088        @Autowired
089        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
090
091        private int mySyncSize = 250;
092
093        @Override
094        @SuppressWarnings({"rawtypes", "unchecked"})
095        public IBundleProvider executeQuery(
096                        SearchParameterMap theParams,
097                        RequestDetails theRequestDetails,
098                        String theSearchUuid,
099                        ISearchBuilder theSb,
100                        Integer theLoadSynchronousUpTo,
101                        RequestPartitionId theRequestPartitionId) {
102                SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequestDetails, theSearchUuid);
103                searchRuntimeDetails.setLoadSynchronous(true);
104
105                boolean theParamWantOnlyCount = isWantOnlyCount(theParams);
106                boolean theParamOrConfigWantCount = nonNull(theParams.getSearchTotalMode())
107                                ? isWantCount(theParams)
108                                : isWantCount(myStorageSettings.getDefaultTotalMode());
109                boolean wantCount = theParamWantOnlyCount || theParamOrConfigWantCount;
110
111                // Execute the query and make sure we return distinct results
112                return myTxService
113                                .withRequest(theRequestDetails)
114                                .withRequestPartitionId(theRequestPartitionId)
115                                .readOnly()
116                                .execute(() -> {
117                                        // Load the results synchronously
118                                        List<JpaPid> pids = new ArrayList<>();
119
120                                        Long count = 0L;
121                                        if (wantCount) {
122
123                                                ourLog.trace("Performing count");
124                                                // TODO FulltextSearchSvcImpl will remove necessary parameters from the "theParams", this will
125                                                // cause actual query after count to
126                                                //  return wrong response. This is some dirty fix to avoid that issue. Params should not be
127                                                // mutated?
128                                                //  Maybe instead of removing them we could skip them in db query builder if full text search
129                                                // was used?
130                                                List<List<IQueryParameterType>> contentAndTerms = theParams.get(Constants.PARAM_CONTENT);
131                                                List<List<IQueryParameterType>> textAndTerms = theParams.get(Constants.PARAM_TEXT);
132
133                                                count = theSb.createCountQuery(
134                                                                theParams, theSearchUuid, theRequestDetails, theRequestPartitionId);
135
136                                                if (contentAndTerms != null) theParams.put(Constants.PARAM_CONTENT, contentAndTerms);
137                                                if (textAndTerms != null) theParams.put(Constants.PARAM_TEXT, textAndTerms);
138
139                                                ourLog.trace("Got count {}", count);
140                                        }
141
142                                        if (theParamWantOnlyCount) {
143                                                SimpleBundleProvider bundleProvider = new SimpleBundleProvider();
144                                                bundleProvider.setSize(count.intValue());
145                                                return bundleProvider;
146                                        }
147
148                                        // if we have a count, we'll want to request
149                                        // additional resources
150                                        SearchParameterMap clonedParams = theParams.clone();
151                                        Integer requestedCount = clonedParams.getCount();
152                                        boolean hasACount = requestedCount != null;
153                                        if (hasACount) {
154                                                clonedParams.setCount(requestedCount.intValue() + 1);
155                                        }
156
157                                        try (IResultIterator<JpaPid> resultIter = theSb.createQuery(
158                                                        clonedParams, searchRuntimeDetails, theRequestDetails, theRequestPartitionId)) {
159                                                while (resultIter.hasNext()) {
160                                                        pids.add(resultIter.next());
161                                                        if (theLoadSynchronousUpTo != null && pids.size() >= theLoadSynchronousUpTo) {
162                                                                break;
163                                                        }
164                                                        if (theParams.getLoadSynchronousUpTo() != null
165                                                                        && pids.size() >= theParams.getLoadSynchronousUpTo()) {
166                                                                break;
167                                                        }
168                                                }
169                                        } catch (IOException e) {
170                                                ourLog.error("IO failure during database access", e);
171                                                throw new InternalErrorException(Msg.code(1164) + e);
172                                        }
173
174                                        // truncate the list we retrieved - if needed
175                                        int receivedResourceCount = -1;
176                                        if (hasACount) {
177                                                // we want the accurate received resource count
178                                                receivedResourceCount = pids.size();
179                                                int resourcesToReturn = Math.min(theParams.getCount(), pids.size());
180                                                pids = pids.subList(0, resourcesToReturn);
181                                        }
182
183                                        JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(pids, () -> theSb);
184                                        HookParams params = new HookParams()
185                                                        .add(IPreResourceAccessDetails.class, accessDetails)
186                                                        .add(RequestDetails.class, theRequestDetails)
187                                                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
188                                        CompositeInterceptorBroadcaster.doCallHooks(
189                                                        myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
190
191                                        for (int i = pids.size() - 1; i >= 0; i--) {
192                                                if (accessDetails.isDontReturnResourceAtIndex(i)) {
193                                                        pids.remove(i);
194                                                }
195                                        }
196
197                                        /*
198                                         * For synchronous queries, we load all the includes right away
199                                         * since we're returning a static bundle with all the results
200                                         * pre-loaded. This is ok because synchronous requests are not
201                                         * expected to be paged
202                                         *
203                                         * On the other hand for async queries we load includes/revincludes
204                                         * individually for pages as we return them to clients
205                                         */
206
207                                        // _includes
208                                        Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage();
209                                        final Set<JpaPid> includedPids = theSb.loadIncludes(
210                                                        myContext,
211                                                        myEntityManager,
212                                                        pids,
213                                                        theParams.getRevIncludes(),
214                                                        true,
215                                                        theParams.getLastUpdated(),
216                                                        "(synchronous)",
217                                                        theRequestDetails,
218                                                        maxIncludes);
219                                        if (maxIncludes != null) {
220                                                maxIncludes -= includedPids.size();
221                                        }
222                                        pids.addAll(includedPids);
223                                        List<JpaPid> includedPidsList = new ArrayList<>(includedPids);
224
225                                        // _revincludes
226                                        if (theParams.getEverythingMode() == null && (maxIncludes == null || maxIncludes > 0)) {
227                                                Set<JpaPid> revIncludedPids = theSb.loadIncludes(
228                                                                myContext,
229                                                                myEntityManager,
230                                                                pids,
231                                                                theParams.getIncludes(),
232                                                                false,
233                                                                theParams.getLastUpdated(),
234                                                                "(synchronous)",
235                                                                theRequestDetails,
236                                                                maxIncludes);
237                                                includedPids.addAll(revIncludedPids);
238                                                pids.addAll(revIncludedPids);
239                                                includedPidsList.addAll(revIncludedPids);
240                                        }
241
242                                        List<IBaseResource> resources = new ArrayList<>();
243                                        theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
244                                        // Hook: STORAGE_PRESHOW_RESOURCES
245                                        resources = ServerInterceptorUtil.fireStoragePreshowResource(
246                                                        resources, theRequestDetails, myInterceptorBroadcaster);
247
248                                        SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
249                                        if (hasACount) {
250                                                bundleProvider.setTotalResourcesRequestedReturned(receivedResourceCount);
251                                        }
252                                        if (theParams.isOffsetQuery()) {
253                                                bundleProvider.setCurrentPageOffset(theParams.getOffset());
254                                                bundleProvider.setCurrentPageSize(theParams.getCount());
255                                        }
256
257                                        if (wantCount) {
258                                                bundleProvider.setSize(count.intValue());
259                                        } else {
260                                                Integer queryCount = getQueryCount(theLoadSynchronousUpTo, theParams);
261                                                if (queryCount == null || queryCount > resources.size()) {
262                                                        // No limit, last page or everything was fetched within the limit
263                                                        bundleProvider.setSize(getTotalCount(queryCount, theParams.getOffset(), resources.size()));
264                                                } else {
265                                                        bundleProvider.setSize(null);
266                                                }
267                                        }
268
269                                        bundleProvider.setPreferredPageSize(theParams.getCount());
270
271                                        return bundleProvider;
272                                });
273        }
274
275        @Override
276        public IBundleProvider executeQuery(
277                        String theResourceType,
278                        SearchParameterMap theSearchParameterMap,
279                        RequestPartitionId theRequestPartitionId) {
280                final String searchUuid = UUID.randomUUID().toString();
281
282                IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(theResourceType);
283
284                Class<? extends IBaseResource> resourceTypeClass =
285                                myContext.getResourceDefinition(theResourceType).getImplementingClass();
286                final ISearchBuilder sb =
287                                mySearchBuilderFactory.newSearchBuilder(callingDao, theResourceType, resourceTypeClass);
288                sb.setFetchSize(mySyncSize);
289                return executeQuery(
290                                theSearchParameterMap,
291                                null,
292                                searchUuid,
293                                sb,
294                                theSearchParameterMap.getLoadSynchronousUpTo(),
295                                theRequestPartitionId);
296        }
297
298        @Autowired
299        public void setContext(FhirContext theContext) {
300                myContext = theContext;
301        }
302
303        private int getTotalCount(Integer queryCount, Integer offset, int queryResultCount) {
304                if (queryCount != null) {
305                        if (offset != null) {
306                                return offset + queryResultCount;
307                        } else {
308                                return queryResultCount;
309                        }
310                } else {
311                        return queryResultCount;
312                }
313        }
314
315        private Integer getQueryCount(Integer theLoadSynchronousUpTo, SearchParameterMap theParams) {
316                if (theLoadSynchronousUpTo != null) {
317                        return theLoadSynchronousUpTo;
318                } else if (theParams.getCount() != null) {
319                        return theParams.getCount();
320                } else if (myStorageSettings.getFetchSizeDefaultMaximum() != null) {
321                        return myStorageSettings.getFetchSizeDefaultMaximum();
322                }
323                return null;
324        }
325}