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