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}