
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}