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}