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 javax.persistence.EntityManager;
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        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(SearchParameterMap theParams, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, Integer theLoadSynchronousUpTo, RequestPartitionId theRequestPartitionId) {
095                SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequestDetails, theSearchUuid);
096                searchRuntimeDetails.setLoadSynchronous(true);
097
098                boolean theParamWantOnlyCount = isWantOnlyCount(theParams);
099                boolean theParamOrConfigWantCount = nonNull(theParams.getSearchTotalMode()) ? isWantCount(theParams) : isWantCount(myStorageSettings.getDefaultTotalMode());
100                boolean wantCount = theParamWantOnlyCount || theParamOrConfigWantCount;
101
102                // Execute the query and make sure we return distinct results
103                return myTxService
104                        .withRequest(theRequestDetails)
105                        .withRequestPartitionId(theRequestPartitionId)
106                        .readOnly()
107                        .execute(() -> {
108
109                        // Load the results synchronously
110                        final List<JpaPid> pids = new ArrayList<>();
111
112                        Long count = 0L;
113                        if (wantCount) {
114
115                                ourLog.trace("Performing count");
116                                // TODO FulltextSearchSvcImpl will remove necessary parameters from the "theParams", this will cause actual query after count to
117                                //  return wrong response. This is some dirty fix to avoid that issue. Params should not be mutated?
118                                //  Maybe instead of removing them we could skip them in db query builder if full text search was used?
119                                List<List<IQueryParameterType>> contentAndTerms = theParams.get(Constants.PARAM_CONTENT);
120                                List<List<IQueryParameterType>> textAndTerms = theParams.get(Constants.PARAM_TEXT);
121
122                                count = theSb.createCountQuery(theParams, theSearchUuid, theRequestDetails, theRequestPartitionId);
123
124                                if (contentAndTerms != null) theParams.put(Constants.PARAM_CONTENT, contentAndTerms);
125                                if (textAndTerms != null) theParams.put(Constants.PARAM_TEXT, textAndTerms);
126
127                                ourLog.trace("Got count {}", count);
128                        }
129
130                        if (theParamWantOnlyCount) {
131                                SimpleBundleProvider bundleProvider = new SimpleBundleProvider();
132                                bundleProvider.setSize(count.intValue());
133                                return bundleProvider;
134                        }
135
136                        try (IResultIterator<JpaPid> resultIter = theSb.createQuery(theParams, searchRuntimeDetails, theRequestDetails, theRequestPartitionId)) {
137                                while (resultIter.hasNext()) {
138                                        pids.add(resultIter.next());
139                                        if (theLoadSynchronousUpTo != null && pids.size() >= theLoadSynchronousUpTo) {
140                                                break;
141                                        }
142                                        if (theParams.getLoadSynchronousUpTo() != null && pids.size() >= theParams.getLoadSynchronousUpTo()) {
143                                                break;
144                                        }
145                                }
146                        } catch (IOException e) {
147                                ourLog.error("IO failure during database access", e);
148                                throw new InternalErrorException(Msg.code(1164) + e);
149                        }
150
151                        JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(pids, () -> theSb);
152                        HookParams params = new HookParams()
153                                .add(IPreResourceAccessDetails.class, accessDetails)
154                                .add(RequestDetails.class, theRequestDetails)
155                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
156                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
157
158                        for (int i = pids.size() - 1; i >= 0; i--) {
159                                if (accessDetails.isDontReturnResourceAtIndex(i)) {
160                                        pids.remove(i);
161                                }
162                        }
163
164                        /*
165                         * For synchronous queries, we load all the includes right away
166                         * since we're returning a static bundle with all the results
167                         * pre-loaded. This is ok because synchronous requests are not
168                         * expected to be paged
169                         *
170                         * On the other hand for async queries we load includes/revincludes
171                         * individually for pages as we return them to clients
172                         */
173
174                        // _includes
175                        Integer maxIncludes = myStorageSettings.getMaximumIncludesToLoadPerPage();
176                        final Set<JpaPid> includedPids = theSb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)", theRequestDetails, maxIncludes);
177                        if (maxIncludes != null) {
178                                maxIncludes -= includedPids.size();
179                        }
180                        pids.addAll(includedPids);
181                        List<JpaPid> includedPidsList = new ArrayList<>(includedPids);
182
183                        // _revincludes
184                        if (theParams.getEverythingMode() == null && (maxIncludes == null || maxIncludes > 0)) {
185                                Set<JpaPid> revIncludedPids = theSb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)", theRequestDetails, maxIncludes);
186                                includedPids.addAll(revIncludedPids);
187                                pids.addAll(revIncludedPids);
188                                includedPidsList.addAll(revIncludedPids);
189                        }
190
191                        List<IBaseResource> resources = new ArrayList<>();
192                        theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
193                        // Hook: STORAGE_PRESHOW_RESOURCES
194                        resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
195
196                        SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
197                        if (theParams.isOffsetQuery()) {
198                                bundleProvider.setCurrentPageOffset(theParams.getOffset());
199                                bundleProvider.setCurrentPageSize(theParams.getCount());
200                        }
201
202                        if (wantCount) {
203                                bundleProvider.setSize(count.intValue());
204                        } else {
205                                Integer queryCount = getQueryCount(theLoadSynchronousUpTo, theParams);
206                                if (queryCount == null || queryCount > resources.size()) {
207                                        // No limit, last page or everything was fetched within the limit
208                                        bundleProvider.setSize(getTotalCount(queryCount, theParams.getOffset(), resources.size()));
209                                } else {
210                                        bundleProvider.setSize(null);
211                                }
212                        }
213
214                        bundleProvider.setPreferredPageSize(theParams.getCount());
215
216                        return bundleProvider;
217                });
218        }
219
220        @Override
221        public IBundleProvider executeQuery(String theResourceType, SearchParameterMap theSearchParameterMap, RequestPartitionId theRequestPartitionId) {
222                final String searchUuid = UUID.randomUUID().toString();
223
224                IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(theResourceType);
225
226                Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass();
227                final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(callingDao, theResourceType, resourceTypeClass);
228                sb.setFetchSize(mySyncSize);
229                return executeQuery(theSearchParameterMap, null, searchUuid, sb, theSearchParameterMap.getLoadSynchronousUpTo(), theRequestPartitionId);
230        }
231
232        @Autowired
233        public void setContext(FhirContext theContext) {
234                myContext = theContext;
235        }
236
237        private int getTotalCount(Integer queryCount, Integer offset, int queryResultCount) {
238                if (queryCount != null) {
239                        if (offset != null) {
240                                return offset + queryResultCount;
241                        } else {
242                                return queryResultCount;
243                        }
244                } else {
245                        return queryResultCount;
246                }
247        }
248
249        private Integer getQueryCount(Integer theLoadSynchronousUpTo, SearchParameterMap theParams) {
250                if (theLoadSynchronousUpTo != null) {
251                        return theLoadSynchronousUpTo;
252                } else if (theParams.getCount() != null) {
253                        return theParams.getCount();
254                } else if (myStorageSettings.getFetchSizeDefaultMaximum() != null) {
255                        return myStorageSettings.getFetchSizeDefaultMaximum();
256                }
257                return null;
258        }
259}