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}