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.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 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;
058import javax.persistence.EntityManager;
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        private 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        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
117                                        // Load the results synchronously
118                                        final 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                                        try (IResultIterator<JpaPid> resultIter = theSb.createQuery(
149                                                        theParams, searchRuntimeDetails, theRequestDetails, theRequestPartitionId)) {
150                                                while (resultIter.hasNext()) {
151                                                        pids.add(resultIter.next());
152                                                        if (theLoadSynchronousUpTo != null && pids.size() >= theLoadSynchronousUpTo) {
153                                                                break;
154                                                        }
155                                                        if (theParams.getLoadSynchronousUpTo() != null
156                                                                        && pids.size() >= theParams.getLoadSynchronousUpTo()) {
157                                                                break;
158                                                        }
159                                                }
160                                        } catch (IOException e) {
161                                                ourLog.error("IO failure during database access", e);
162                                                throw new InternalErrorException(Msg.code(1164) + e);
163                                        }
164
165                                        JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(pids, () -> theSb);
166                                        HookParams params = new HookParams()
167                                                        .add(IPreResourceAccessDetails.class, accessDetails)
168                                                        .add(RequestDetails.class, theRequestDetails)
169                                                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
170                                        CompositeInterceptorBroadcaster.doCallHooks(
171                                                        myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
172
173                                        for (int i = pids.size() - 1; i >= 0; i--) {
174                                                if (accessDetails.isDontReturnResourceAtIndex(i)) {
175                                                        pids.remove(i);
176                                                }
177                                        }
178
179                                        /*
180                                         * For synchronous queries, we load all the includes right away
181                                         * since we're returning a static bundle with all the results
182                                         * pre-loaded. This is ok because synchronous requests are not
183                                         * expected to be paged
184                                         *
185                                         * On the other hand for async queries we load includes/revincludes
186                                         * individually for pages as we return them to clients
187                                         */
188
189                                        // _includes
190                                        Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage();
191                                        final Set<JpaPid> includedPids = theSb.loadIncludes(
192                                                        myContext,
193                                                        myEntityManager,
194                                                        pids,
195                                                        theParams.getRevIncludes(),
196                                                        true,
197                                                        theParams.getLastUpdated(),
198                                                        "(synchronous)",
199                                                        theRequestDetails,
200                                                        maxIncludes);
201                                        if (maxIncludes != null) {
202                                                maxIncludes -= includedPids.size();
203                                        }
204                                        pids.addAll(includedPids);
205                                        List<JpaPid> includedPidsList = new ArrayList<>(includedPids);
206
207                                        // _revincludes
208                                        if (theParams.getEverythingMode() == null && (maxIncludes == null || maxIncludes > 0)) {
209                                                Set<JpaPid> revIncludedPids = theSb.loadIncludes(
210                                                                myContext,
211                                                                myEntityManager,
212                                                                pids,
213                                                                theParams.getIncludes(),
214                                                                false,
215                                                                theParams.getLastUpdated(),
216                                                                "(synchronous)",
217                                                                theRequestDetails,
218                                                                maxIncludes);
219                                                includedPids.addAll(revIncludedPids);
220                                                pids.addAll(revIncludedPids);
221                                                includedPidsList.addAll(revIncludedPids);
222                                        }
223
224                                        List<IBaseResource> resources = new ArrayList<>();
225                                        theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
226                                        // Hook: STORAGE_PRESHOW_RESOURCES
227                                        resources = ServerInterceptorUtil.fireStoragePreshowResource(
228                                                        resources, theRequestDetails, myInterceptorBroadcaster);
229
230                                        SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
231                                        if (theParams.isOffsetQuery()) {
232                                                bundleProvider.setCurrentPageOffset(theParams.getOffset());
233                                                bundleProvider.setCurrentPageSize(theParams.getCount());
234                                        }
235
236                                        if (wantCount) {
237                                                bundleProvider.setSize(count.intValue());
238                                        } else {
239                                                Integer queryCount = getQueryCount(theLoadSynchronousUpTo, theParams);
240                                                if (queryCount == null || queryCount > resources.size()) {
241                                                        // No limit, last page or everything was fetched within the limit
242                                                        bundleProvider.setSize(getTotalCount(queryCount, theParams.getOffset(), resources.size()));
243                                                } else {
244                                                        bundleProvider.setSize(null);
245                                                }
246                                        }
247
248                                        bundleProvider.setPreferredPageSize(theParams.getCount());
249
250                                        return bundleProvider;
251                                });
252        }
253
254        @Override
255        public IBundleProvider executeQuery(
256                        String theResourceType,
257                        SearchParameterMap theSearchParameterMap,
258                        RequestPartitionId theRequestPartitionId) {
259                final String searchUuid = UUID.randomUUID().toString();
260
261                IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(theResourceType);
262
263                Class<? extends IBaseResource> resourceTypeClass =
264                                myContext.getResourceDefinition(theResourceType).getImplementingClass();
265                final ISearchBuilder sb =
266                                mySearchBuilderFactory.newSearchBuilder(callingDao, theResourceType, resourceTypeClass);
267                sb.setFetchSize(mySyncSize);
268                return executeQuery(
269                                theSearchParameterMap,
270                                null,
271                                searchUuid,
272                                sb,
273                                theSearchParameterMap.getLoadSynchronousUpTo(),
274                                theRequestPartitionId);
275        }
276
277        @Autowired
278        public void setContext(FhirContext theContext) {
279                myContext = theContext;
280        }
281
282        private int getTotalCount(Integer queryCount, Integer offset, int queryResultCount) {
283                if (queryCount != null) {
284                        if (offset != null) {
285                                return offset + queryResultCount;
286                        } else {
287                                return queryResultCount;
288                        }
289                } else {
290                        return queryResultCount;
291                }
292        }
293
294        private Integer getQueryCount(Integer theLoadSynchronousUpTo, SearchParameterMap theParams) {
295                if (theLoadSynchronousUpTo != null) {
296                        return theLoadSynchronousUpTo;
297                } else if (theParams.getCount() != null) {
298                        return theParams.getCount();
299                } else if (myStorageSettings.getFetchSizeDefaultMaximum() != null) {
300                        return myStorageSettings.getFetchSizeDefaultMaximum();
301                }
302                return null;
303        }
304}