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.builder; 021 022import ca.uhn.fhir.context.ComboSearchParamType; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.context.RuntimeResourceDefinition; 026import ca.uhn.fhir.context.RuntimeSearchParam; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.interceptor.api.HookParams; 029import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 030import ca.uhn.fhir.interceptor.api.Pointcut; 031import ca.uhn.fhir.interceptor.model.RequestPartitionId; 032import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 033import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 034import ca.uhn.fhir.jpa.api.dao.IDao; 035import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 036import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 037import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; 038import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; 039import ca.uhn.fhir.jpa.dao.BaseStorageDao; 040import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; 041import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 042import ca.uhn.fhir.jpa.dao.IResultIterator; 043import ca.uhn.fhir.jpa.dao.ISearchBuilder; 044import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao; 045import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; 046import ca.uhn.fhir.jpa.dao.search.ResourceNotFoundInIndexException; 047import ca.uhn.fhir.jpa.entity.ResourceSearchView; 048import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails; 049import ca.uhn.fhir.jpa.model.config.PartitionSettings; 050import ca.uhn.fhir.jpa.model.dao.JpaPid; 051import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 052import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; 053import ca.uhn.fhir.jpa.model.entity.ResourceTag; 054import ca.uhn.fhir.jpa.model.search.SearchBuilderLoadIncludesParameters; 055import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; 056import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 057import ca.uhn.fhir.jpa.search.SearchConstants; 058import ca.uhn.fhir.jpa.search.builder.sql.GeneratedSql; 059import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 060import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryExecutor; 061import ca.uhn.fhir.jpa.search.builder.sql.SqlObjectFactory; 062import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc; 063import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 064import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper; 065import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 066import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper; 067import ca.uhn.fhir.jpa.util.BaseIterator; 068import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; 069import ca.uhn.fhir.jpa.util.QueryChunker; 070import ca.uhn.fhir.jpa.util.SqlQueryList; 071import ca.uhn.fhir.model.api.IQueryParameterType; 072import ca.uhn.fhir.model.api.Include; 073import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 074import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; 075import ca.uhn.fhir.rest.api.Constants; 076import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 077import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 078import ca.uhn.fhir.rest.api.SortOrderEnum; 079import ca.uhn.fhir.rest.api.SortSpec; 080import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 081import ca.uhn.fhir.rest.api.server.RequestDetails; 082import ca.uhn.fhir.rest.param.DateRangeParam; 083import ca.uhn.fhir.rest.param.ParameterUtil; 084import ca.uhn.fhir.rest.param.ReferenceParam; 085import ca.uhn.fhir.rest.param.StringParam; 086import ca.uhn.fhir.rest.param.TokenParam; 087import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 088import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 089import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 090import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 091import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 092import ca.uhn.fhir.util.StopWatch; 093import ca.uhn.fhir.util.StringUtil; 094import ca.uhn.fhir.util.UrlUtil; 095import com.google.common.collect.Streams; 096import com.healthmarketscience.sqlbuilder.Condition; 097import org.apache.commons.lang3.StringUtils; 098import org.apache.commons.lang3.Validate; 099import org.apache.commons.lang3.math.NumberUtils; 100import org.apache.commons.lang3.tuple.Pair; 101import org.hl7.fhir.instance.model.api.IAnyResource; 102import org.hl7.fhir.instance.model.api.IBaseResource; 103import org.slf4j.Logger; 104import org.slf4j.LoggerFactory; 105import org.springframework.beans.factory.annotation.Autowired; 106import org.springframework.jdbc.core.JdbcTemplate; 107import org.springframework.jdbc.core.SingleColumnRowMapper; 108import org.springframework.transaction.support.TransactionSynchronizationManager; 109 110import java.util.ArrayList; 111import java.util.Collection; 112import java.util.Collections; 113import java.util.HashMap; 114import java.util.HashSet; 115import java.util.Iterator; 116import java.util.List; 117import java.util.Map; 118import java.util.Objects; 119import java.util.Optional; 120import java.util.Set; 121import java.util.stream.Collectors; 122import javax.annotation.Nonnull; 123import javax.annotation.Nullable; 124import javax.persistence.EntityManager; 125import javax.persistence.PersistenceContext; 126import javax.persistence.PersistenceContextType; 127import javax.persistence.Query; 128import javax.persistence.Tuple; 129import javax.persistence.TypedQuery; 130import javax.persistence.criteria.CriteriaBuilder; 131 132import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION; 133import static org.apache.commons.lang3.StringUtils.defaultString; 134import static org.apache.commons.lang3.StringUtils.isBlank; 135import static org.apache.commons.lang3.StringUtils.isNotBlank; 136 137/** 138 * The SearchBuilder is responsible for actually forming the SQL query that handles 139 * searches for resources 140 */ 141public class SearchBuilder implements ISearchBuilder<JpaPid> { 142 143 /** 144 * See loadResourcesByPid 145 * for an explanation of why we use the constant 800 146 */ 147 // NB: keep public 148 @Deprecated 149 public static final int MAXIMUM_PAGE_SIZE = SearchConstants.MAX_PAGE_SIZE; 150 151 public static final int MAXIMUM_PAGE_SIZE_FOR_TESTING = 50; 152 public static final String RESOURCE_ID_ALIAS = "resource_id"; 153 public static final String RESOURCE_VERSION_ALIAS = "resource_version"; 154 private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class); 155 private static final JpaPid NO_MORE = JpaPid.fromId(-1L); 156 private static final String MY_TARGET_RESOURCE_PID = "myTargetResourcePid"; 157 private static final String MY_SOURCE_RESOURCE_PID = "mySourceResourcePid"; 158 private static final String MY_TARGET_RESOURCE_TYPE = "myTargetResourceType"; 159 private static final String MY_SOURCE_RESOURCE_TYPE = "mySourceResourceType"; 160 private static final String MY_TARGET_RESOURCE_VERSION = "myTargetResourceVersion"; 161 public static boolean myUseMaxPageSize50ForTest = false; 162 protected final IInterceptorBroadcaster myInterceptorBroadcaster; 163 protected final IResourceTagDao myResourceTagDao; 164 private final String myResourceName; 165 private final Class<? extends IBaseResource> myResourceType; 166 private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory; 167 private final SqlObjectFactory mySqlBuilderFactory; 168 private final HibernatePropertiesProvider myDialectProvider; 169 private final ISearchParamRegistry mySearchParamRegistry; 170 private final PartitionSettings myPartitionSettings; 171 private final DaoRegistry myDaoRegistry; 172 private final IResourceSearchViewDao myResourceSearchViewDao; 173 private final FhirContext myContext; 174 private final IIdHelperService<JpaPid> myIdHelperService; 175 private final JpaStorageSettings myStorageSettings; 176 private final IDao myCallingDao; 177 178 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 179 protected EntityManager myEntityManager; 180 181 private List<JpaPid> myAlsoIncludePids; 182 private CriteriaBuilder myCriteriaBuilder; 183 private SearchParameterMap myParams; 184 private String mySearchUuid; 185 private int myFetchSize; 186 private Integer myMaxResultsToFetch; 187 private Set<JpaPid> myPidSet; 188 private boolean myHasNextIteratorQuery = false; 189 private RequestPartitionId myRequestPartitionId; 190 191 @Autowired(required = false) 192 private IFulltextSearchSvc myFulltextSearchSvc; 193 194 @Autowired(required = false) 195 private IElasticsearchSvc myIElasticsearchSvc; 196 197 @Autowired 198 private IJpaStorageResourceParser myJpaStorageResourceParser; 199 200 /** 201 * Constructor 202 */ 203 public SearchBuilder( 204 IDao theDao, 205 String theResourceName, 206 JpaStorageSettings theStorageSettings, 207 HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory, 208 SqlObjectFactory theSqlBuilderFactory, 209 HibernatePropertiesProvider theDialectProvider, 210 ISearchParamRegistry theSearchParamRegistry, 211 PartitionSettings thePartitionSettings, 212 IInterceptorBroadcaster theInterceptorBroadcaster, 213 IResourceTagDao theResourceTagDao, 214 DaoRegistry theDaoRegistry, 215 IResourceSearchViewDao theResourceSearchViewDao, 216 FhirContext theContext, 217 IIdHelperService theIdHelperService, 218 Class<? extends IBaseResource> theResourceType) { 219 myCallingDao = theDao; 220 myResourceName = theResourceName; 221 myResourceType = theResourceType; 222 myStorageSettings = theStorageSettings; 223 224 myEntityManagerFactory = theEntityManagerFactory; 225 mySqlBuilderFactory = theSqlBuilderFactory; 226 myDialectProvider = theDialectProvider; 227 mySearchParamRegistry = theSearchParamRegistry; 228 myPartitionSettings = thePartitionSettings; 229 myInterceptorBroadcaster = theInterceptorBroadcaster; 230 myResourceTagDao = theResourceTagDao; 231 myDaoRegistry = theDaoRegistry; 232 myResourceSearchViewDao = theResourceSearchViewDao; 233 myContext = theContext; 234 myIdHelperService = theIdHelperService; 235 } 236 237 @Override 238 public void setMaxResultsToFetch(Integer theMaxResultsToFetch) { 239 myMaxResultsToFetch = theMaxResultsToFetch; 240 } 241 242 private void searchForIdsWithAndOr( 243 SearchQueryBuilder theSearchSqlBuilder, 244 QueryStack theQueryStack, 245 @Nonnull SearchParameterMap theParams, 246 RequestDetails theRequest) { 247 myParams = theParams; 248 249 // Remove any empty parameters 250 theParams.clean(); 251 252 // For DSTU3, pull out near-distance first so when it comes time to evaluate near, we already know the distance 253 if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { 254 Dstu3DistanceHelper.setNearDistance(myResourceType, theParams); 255 } 256 257 // Attempt to lookup via composite unique key. 258 if (isCompositeUniqueSpCandidate()) { 259 attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest); 260 } 261 262 SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode(); 263 264 // Handle _id and _tag last, since they can typically be tacked onto a different parameter 265 List<String> paramNames = myParams.keySet().stream() 266 .filter(t -> !t.equals(IAnyResource.SP_RES_ID)) 267 .filter(t -> !t.equals(Constants.PARAM_TAG)) 268 .collect(Collectors.toList()); 269 if (myParams.containsKey(IAnyResource.SP_RES_ID)) { 270 paramNames.add(IAnyResource.SP_RES_ID); 271 } 272 if (myParams.containsKey(Constants.PARAM_TAG)) { 273 paramNames.add(Constants.PARAM_TAG); 274 } 275 276 // Handle each parameter 277 for (String nextParamName : paramNames) { 278 if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) { 279 // Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by 280 // Elasticsearch 281 continue; 282 } 283 List<List<IQueryParameterType>> andOrParams = myParams.get(nextParamName); 284 Condition predicate = theQueryStack.searchForIdsWithAndOr( 285 null, 286 myResourceName, 287 nextParamName, 288 andOrParams, 289 theRequest, 290 myRequestPartitionId, 291 searchContainedMode); 292 if (predicate != null) { 293 theSearchSqlBuilder.addPredicate(predicate); 294 } 295 } 296 } 297 298 /** 299 * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the 300 * parameters all have no modifiers. 301 */ 302 private boolean isCompositeUniqueSpCandidate() { 303 return myStorageSettings.isUniqueIndexesEnabled() 304 && myParams.getEverythingMode() == null 305 && myParams.isAllParametersHaveNoModifier(); 306 } 307 308 @SuppressWarnings("ConstantConditions") 309 @Override 310 public Long createCountQuery( 311 SearchParameterMap theParams, 312 String theSearchUuid, 313 RequestDetails theRequest, 314 @Nonnull RequestPartitionId theRequestPartitionId) { 315 316 assert theRequestPartitionId != null; 317 assert TransactionSynchronizationManager.isActualTransactionActive(); 318 319 init(theParams, theSearchUuid, theRequestPartitionId); 320 321 if (checkUseHibernateSearch()) { 322 long count = myFulltextSearchSvc.count(myResourceName, theParams.clone()); 323 return count; 324 } 325 326 List<ISearchQueryExecutor> queries = createQuery(theParams.clone(), null, null, null, true, theRequest, null); 327 if (queries.isEmpty()) { 328 return 0L; 329 } else { 330 return queries.get(0).next(); 331 } 332 } 333 334 /** 335 * @param thePidSet May be null 336 */ 337 @Override 338 public void setPreviouslyAddedResourcePids(@Nonnull List<JpaPid> thePidSet) { 339 myPidSet = new HashSet<>(thePidSet); 340 } 341 342 @SuppressWarnings("ConstantConditions") 343 @Override 344 public IResultIterator createQuery( 345 SearchParameterMap theParams, 346 SearchRuntimeDetails theSearchRuntimeDetails, 347 RequestDetails theRequest, 348 @Nonnull RequestPartitionId theRequestPartitionId) { 349 assert theRequestPartitionId != null; 350 assert TransactionSynchronizationManager.isActualTransactionActive(); 351 352 init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId); 353 354 if (myPidSet == null) { 355 myPidSet = new HashSet<>(); 356 } 357 358 return new QueryIterator(theSearchRuntimeDetails, theRequest); 359 } 360 361 private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) { 362 myCriteriaBuilder = myEntityManager.getCriteriaBuilder(); 363 // we mutate the params. Make a private copy. 364 myParams = theParams.clone(); 365 mySearchUuid = theSearchUuid; 366 myRequestPartitionId = theRequestPartitionId; 367 } 368 369 private List<ISearchQueryExecutor> createQuery( 370 SearchParameterMap theParams, 371 SortSpec sort, 372 Integer theOffset, 373 Integer theMaximumResults, 374 boolean theCountOnlyFlag, 375 RequestDetails theRequest, 376 SearchRuntimeDetails theSearchRuntimeDetails) { 377 378 ArrayList<ISearchQueryExecutor> queries = new ArrayList<>(); 379 380 if (checkUseHibernateSearch()) { 381 // we're going to run at least part of the search against the Fulltext service. 382 383 // Ugh - we have two different return types for now 384 ISearchQueryExecutor fulltextExecutor = null; 385 List<JpaPid> fulltextMatchIds = null; 386 int resultCount = 0; 387 if (myParams.isLastN()) { 388 fulltextMatchIds = executeLastNAgainstIndex(theMaximumResults); 389 resultCount = fulltextMatchIds.size(); 390 } else if (myParams.getEverythingMode() != null) { 391 fulltextMatchIds = queryHibernateSearchForEverythingPids(theRequest); 392 resultCount = fulltextMatchIds.size(); 393 } else { 394 fulltextExecutor = myFulltextSearchSvc.searchNotScrolled( 395 myResourceName, myParams, myMaxResultsToFetch, theRequest); 396 } 397 398 if (fulltextExecutor == null) { 399 fulltextExecutor = SearchQueryExecutors.from(fulltextMatchIds); 400 } 401 402 if (theSearchRuntimeDetails != null) { 403 theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount); 404 HookParams params = new HookParams() 405 .add(RequestDetails.class, theRequest) 406 .addIfMatchesType(ServletRequestDetails.class, theRequest) 407 .add(SearchRuntimeDetails.class, theSearchRuntimeDetails); 408 CompositeInterceptorBroadcaster.doCallHooks( 409 myInterceptorBroadcaster, 410 theRequest, 411 Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, 412 params); 413 } 414 415 // can we skip the database entirely and return the pid list from here? 416 boolean canSkipDatabase = 417 // if we processed an AND clause, and it returned nothing, then nothing can match. 418 !fulltextExecutor.hasNext() 419 || 420 // Our hibernate search query doesn't respect partitions yet 421 (!myPartitionSettings.isPartitioningEnabled() 422 && 423 // were there AND terms left? Then we still need the db. 424 theParams.isEmpty() 425 && 426 // not every param is a param. :-( 427 theParams.getNearDistanceParam() == null 428 && 429 // todo MB don't we support _lastUpdated and _offset now? 430 theParams.getLastUpdated() == null 431 && theParams.getEverythingMode() == null 432 && theParams.getOffset() == null); 433 434 if (canSkipDatabase) { 435 ourLog.trace("Query finished after HSearch. Skip db query phase"); 436 if (theMaximumResults != null) { 437 fulltextExecutor = SearchQueryExecutors.limited(fulltextExecutor, theMaximumResults); 438 } 439 queries.add(fulltextExecutor); 440 } else { 441 ourLog.trace("Query needs db after HSearch. Chunking."); 442 // Finish the query in the database for the rest of the search parameters, sorting, partitioning, etc. 443 // We break the pids into chunks that fit in the 1k limit for jdbc bind params. 444 new QueryChunker<Long>() 445 .chunk( 446 Streams.stream(fulltextExecutor).collect(Collectors.toList()), 447 t -> doCreateChunkedQueries( 448 theParams, t, theOffset, sort, theCountOnlyFlag, theRequest, queries)); 449 } 450 } else { 451 // do everything in the database. 452 Optional<SearchQueryExecutor> query = createChunkedQuery( 453 theParams, sort, theOffset, theMaximumResults, theCountOnlyFlag, theRequest, null); 454 query.ifPresent(queries::add); 455 } 456 457 return queries; 458 } 459 460 /** 461 * Check to see if query should use Hibernate Search, and error if the query can't continue. 462 * 463 * @return true if the query should first be processed by Hibernate Search 464 * @throws InvalidRequestException if fulltext search is not enabled but the query requires it - _content or _text 465 */ 466 private boolean checkUseHibernateSearch() { 467 boolean fulltextEnabled = (myFulltextSearchSvc != null) && !myFulltextSearchSvc.isDisabled(); 468 469 if (!fulltextEnabled) { 470 failIfUsed(Constants.PARAM_TEXT); 471 failIfUsed(Constants.PARAM_CONTENT); 472 } 473 474 // someday we'll want a query planner to figure out if we _should_ or _must_ use the ft index, not just if we 475 // can. 476 return fulltextEnabled 477 && myParams != null 478 && myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE 479 && myFulltextSearchSvc.supportsSomeOf(myParams); 480 } 481 482 private void failIfUsed(String theParamName) { 483 if (myParams.containsKey(theParamName)) { 484 throw new InvalidRequestException(Msg.code(1192) 485 + "Fulltext search is not enabled on this service, can not process parameter: " + theParamName); 486 } 487 } 488 489 private List<JpaPid> executeLastNAgainstIndex(Integer theMaximumResults) { 490 // Can we use our hibernate search generated index on resource to support lastN?: 491 if (myStorageSettings.isAdvancedHSearchIndexing()) { 492 if (myFulltextSearchSvc == null) { 493 throw new InvalidRequestException(Msg.code(2027) 494 + "LastN operation is not enabled on this service, can not process this request"); 495 } 496 return myFulltextSearchSvc.lastN(myParams, theMaximumResults).stream() 497 .map(lastNResourceId -> myIdHelperService.resolveResourcePersistentIds( 498 myRequestPartitionId, myResourceName, String.valueOf(lastNResourceId))) 499 .collect(Collectors.toList()); 500 } else { 501 if (myIElasticsearchSvc == null) { 502 throw new InvalidRequestException(Msg.code(2033) 503 + "LastN operation is not enabled on this service, can not process this request"); 504 } 505 // use the dedicated observation ES/Lucene index to support lastN query 506 return myIElasticsearchSvc.executeLastN(myParams, myContext, theMaximumResults).stream() 507 .map(lastnResourceId -> myIdHelperService.resolveResourcePersistentIds( 508 myRequestPartitionId, myResourceName, lastnResourceId)) 509 .collect(Collectors.toList()); 510 } 511 } 512 513 private List<JpaPid> queryHibernateSearchForEverythingPids(RequestDetails theRequestDetails) { 514 JpaPid pid = null; 515 if (myParams.get(IAnyResource.SP_RES_ID) != null) { 516 String idParamValue; 517 IQueryParameterType idParam = 518 myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); 519 if (idParam instanceof TokenParam) { 520 TokenParam idParm = (TokenParam) idParam; 521 idParamValue = idParm.getValue(); 522 } else { 523 StringParam idParm = (StringParam) idParam; 524 idParamValue = idParm.getValue(); 525 } 526 527 pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue); 528 } 529 List<JpaPid> pids = myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails); 530 return pids; 531 } 532 533 private void doCreateChunkedQueries( 534 SearchParameterMap theParams, 535 List<Long> thePids, 536 Integer theOffset, 537 SortSpec sort, 538 boolean theCount, 539 RequestDetails theRequest, 540 ArrayList<ISearchQueryExecutor> theQueries) { 541 if (thePids.size() < getMaximumPageSize()) { 542 normalizeIdListForLastNInClause(thePids); 543 } 544 Optional<SearchQueryExecutor> query = 545 createChunkedQuery(theParams, sort, theOffset, thePids.size(), theCount, theRequest, thePids); 546 query.ifPresent(t -> theQueries.add(t)); 547 } 548 549 /** 550 * Combs through the params for any _id parameters and extracts the PIDs for them 551 * 552 * @param theTargetPids 553 */ 554 private void extractTargetPidsFromIdParams(HashSet<Long> theTargetPids) { 555 // get all the IQueryParameterType objects 556 // for _id -> these should all be StringParam values 557 HashSet<String> ids = new HashSet<>(); 558 List<List<IQueryParameterType>> params = myParams.get(IAnyResource.SP_RES_ID); 559 for (List<IQueryParameterType> paramList : params) { 560 for (IQueryParameterType param : paramList) { 561 if (param instanceof StringParam) { 562 // we expect all _id values to be StringParams 563 ids.add(((StringParam) param).getValue()); 564 } else if (param instanceof TokenParam) { 565 ids.add(((TokenParam) param).getValue()); 566 } else { 567 // we do not expect the _id parameter to be a non-string value 568 throw new IllegalArgumentException( 569 Msg.code(1193) + "_id parameter must be a StringParam or TokenParam"); 570 } 571 } 572 } 573 574 // fetch our target Pids 575 // this will throw if an id is not found 576 Map<String, JpaPid> idToPid = myIdHelperService.resolveResourcePersistentIds( 577 myRequestPartitionId, myResourceName, new ArrayList<>(ids)); 578 if (myAlsoIncludePids == null) { 579 myAlsoIncludePids = new ArrayList<>(); 580 } 581 582 // add the pids to targetPids 583 for (JpaPid pid : idToPid.values()) { 584 myAlsoIncludePids.add(pid); 585 theTargetPids.add(pid.getId()); 586 } 587 } 588 589 private Optional<SearchQueryExecutor> createChunkedQuery( 590 SearchParameterMap theParams, 591 SortSpec sort, 592 Integer theOffset, 593 Integer theMaximumResults, 594 boolean theCountOnlyFlag, 595 RequestDetails theRequest, 596 List<Long> thePidList) { 597 String sqlBuilderResourceName = myParams.getEverythingMode() == null ? myResourceName : null; 598 SearchQueryBuilder sqlBuilder = new SearchQueryBuilder( 599 myContext, 600 myStorageSettings, 601 myPartitionSettings, 602 myRequestPartitionId, 603 sqlBuilderResourceName, 604 mySqlBuilderFactory, 605 myDialectProvider, 606 theCountOnlyFlag); 607 QueryStack queryStack3 = new QueryStack( 608 theParams, myStorageSettings, myContext, sqlBuilder, mySearchParamRegistry, myPartitionSettings); 609 610 if (theParams.keySet().size() > 1 611 || theParams.getSort() != null 612 || theParams.keySet().contains(Constants.PARAM_HAS) 613 || isPotentiallyContainedReferenceParameterExistsAtRoot(theParams)) { 614 List<RuntimeSearchParam> activeComboParams = 615 mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet()); 616 if (activeComboParams.isEmpty()) { 617 sqlBuilder.setNeedResourceTableRoot(true); 618 } 619 } 620 621 JdbcTemplate jdbcTemplate = new JdbcTemplate(myEntityManagerFactory.getDataSource()); 622 jdbcTemplate.setFetchSize(myFetchSize); 623 if (theMaximumResults != null) { 624 jdbcTemplate.setMaxRows(theMaximumResults); 625 } 626 627 if (myParams.getEverythingMode() != null) { 628 HashSet<Long> targetPids = new HashSet<>(); 629 if (myParams.get(IAnyResource.SP_RES_ID) != null) { 630 extractTargetPidsFromIdParams(targetPids); 631 } else { 632 // For Everything queries, we make the query root by the ResourceLink table, since this query 633 // is basically a reverse-include search. For type/Everything (as opposed to instance/Everything) 634 // the one problem with this approach is that it doesn't catch Patients that have absolutely 635 // nothing linked to them. So we do one additional query to make sure we catch those too. 636 SearchQueryBuilder fetchPidsSqlBuilder = new SearchQueryBuilder( 637 myContext, 638 myStorageSettings, 639 myPartitionSettings, 640 myRequestPartitionId, 641 myResourceName, 642 mySqlBuilderFactory, 643 myDialectProvider, 644 theCountOnlyFlag); 645 GeneratedSql allTargetsSql = fetchPidsSqlBuilder.generate(theOffset, myMaxResultsToFetch); 646 String sql = allTargetsSql.getSql(); 647 Object[] args = allTargetsSql.getBindVariables().toArray(new Object[0]); 648 List<Long> output = jdbcTemplate.query(sql, args, new SingleColumnRowMapper<>(Long.class)); 649 if (myAlsoIncludePids == null) { 650 myAlsoIncludePids = new ArrayList<>(output.size()); 651 } 652 myAlsoIncludePids.addAll(JpaPid.fromLongList(output)); 653 } 654 655 List<String> typeSourceResources = new ArrayList<>(); 656 if (myParams.get(Constants.PARAM_TYPE) != null) { 657 typeSourceResources.addAll(extractTypeSourceResourcesFromParams()); 658 } 659 660 queryStack3.addPredicateEverythingOperation( 661 myResourceName, typeSourceResources, targetPids.toArray(new Long[0])); 662 } else { 663 /* 664 * If we're doing a filter, always use the resource table as the root - This avoids the possibility of 665 * specific filters with ORs as their root from working around the natural resource type / deletion 666 * status / partition IDs built into queries. 667 */ 668 if (theParams.containsKey(Constants.PARAM_FILTER)) { 669 Condition partitionIdPredicate = sqlBuilder 670 .getOrCreateResourceTablePredicateBuilder() 671 .createPartitionIdPredicate(myRequestPartitionId); 672 if (partitionIdPredicate != null) { 673 sqlBuilder.addPredicate(partitionIdPredicate); 674 } 675 } 676 677 // Normal search 678 searchForIdsWithAndOr(sqlBuilder, queryStack3, myParams, theRequest); 679 } 680 681 // If we haven't added any predicates yet, we're doing a search for all resources. Make sure we add the 682 // partition ID predicate in that case. 683 if (!sqlBuilder.haveAtLeastOnePredicate()) { 684 Condition partitionIdPredicate = sqlBuilder 685 .getOrCreateResourceTablePredicateBuilder() 686 .createPartitionIdPredicate(myRequestPartitionId); 687 if (partitionIdPredicate != null) { 688 sqlBuilder.addPredicate(partitionIdPredicate); 689 } 690 } 691 692 // Add PID list predicate for full text search and/or lastn operation 693 if (thePidList != null && thePidList.size() > 0) { 694 sqlBuilder.addResourceIdsPredicate(thePidList); 695 } 696 697 // Last updated 698 DateRangeParam lu = myParams.getLastUpdated(); 699 if (lu != null && !lu.isEmpty()) { 700 Condition lastUpdatedPredicates = sqlBuilder.addPredicateLastUpdated(lu); 701 sqlBuilder.addPredicate(lastUpdatedPredicates); 702 } 703 704 /* 705 * Exclude the pids already in the previous iterator. This is an optimization, as opposed 706 * to something needed to guarantee correct results. 707 * 708 * Why do we need it? Suppose for example, a query like: 709 * Observation?category=foo,bar,baz 710 * And suppose you have many resources that have all 3 of these category codes. In this case 711 * the SQL query will probably return the same PIDs multiple times, and if this happens enough 712 * we may exhaust the query results without getting enough distinct results back. When that 713 * happens we re-run the query with a larger limit. Excluding results we already know about 714 * tries to ensure that we get new unique results. 715 * 716 * The challenge with that though is that lots of DBs have an issue with too many 717 * parameters in one query. So we only do this optimization if there aren't too 718 * many results. 719 */ 720 if (myHasNextIteratorQuery) { 721 if (myPidSet.size() + sqlBuilder.countBindVariables() < 900) { 722 sqlBuilder.excludeResourceIdsPredicate(myPidSet); 723 } 724 } 725 726 /* 727 * If offset is present, we want deduplicate the results by using GROUP BY 728 */ 729 if (theOffset != null) { 730 queryStack3.addGrouping(); 731 queryStack3.setUseAggregate(true); 732 } 733 734 /* 735 * Sort 736 * 737 * If we have a sort, we wrap the criteria search (the search that actually 738 * finds the appropriate resources) in an outer search which is then sorted 739 */ 740 if (sort != null) { 741 assert !theCountOnlyFlag; 742 743 createSort(queryStack3, sort, theParams); 744 } 745 746 /* 747 * Now perform the search 748 */ 749 GeneratedSql generatedSql = sqlBuilder.generate(theOffset, myMaxResultsToFetch); 750 if (generatedSql.isMatchNothing()) { 751 return Optional.empty(); 752 } 753 754 SearchQueryExecutor executor = mySqlBuilderFactory.newSearchQueryExecutor(generatedSql, myMaxResultsToFetch); 755 return Optional.of(executor); 756 } 757 758 private Collection<String> extractTypeSourceResourcesFromParams() { 759 760 List<List<IQueryParameterType>> listOfList = myParams.get(Constants.PARAM_TYPE); 761 762 // first off, let's flatten the list of list 763 List<IQueryParameterType> iQueryParameterTypesList = 764 listOfList.stream().flatMap(List::stream).collect(Collectors.toList()); 765 766 // then, extract all elements of each CSV into one big list 767 List<String> resourceTypes = iQueryParameterTypesList.stream() 768 .map(param -> ((StringParam) param).getValue()) 769 .map(csvString -> List.of(csvString.split(","))) 770 .flatMap(List::stream) 771 .collect(Collectors.toList()); 772 773 Set<String> knownResourceTypes = myContext.getResourceTypes(); 774 775 // remove leading/trailing whitespaces if any and remove duplicates 776 Set<String> retVal = new HashSet<>(); 777 778 for (String type : resourceTypes) { 779 String trimmed = type.trim(); 780 if (!knownResourceTypes.contains(trimmed)) { 781 throw new ResourceNotFoundException( 782 Msg.code(2197) + "Unknown resource type '" + trimmed + "' in _type parameter."); 783 } 784 retVal.add(trimmed); 785 } 786 787 return retVal; 788 } 789 790 private boolean isPotentiallyContainedReferenceParameterExistsAtRoot(SearchParameterMap theParams) { 791 return myStorageSettings.isIndexOnContainedResources() 792 && theParams.values().stream() 793 .flatMap(Collection::stream) 794 .flatMap(Collection::stream) 795 .anyMatch(t -> t instanceof ReferenceParam); 796 } 797 798 private List<Long> normalizeIdListForLastNInClause(List<Long> lastnResourceIds) { 799 /* 800 The following is a workaround to a known issue involving Hibernate. If queries are used with "in" clauses with large and varying 801 numbers of parameters, this can overwhelm Hibernate's QueryPlanCache and deplete heap space. See the following link for more info: 802 https://stackoverflow.com/questions/31557076/spring-hibernate-query-plan-cache-memory-usage. 803 804 Normalizing the number of parameters in the "in" clause stabilizes the size of the QueryPlanCache, so long as the number of 805 arguments never exceeds the maximum specified below. 806 */ 807 int listSize = lastnResourceIds.size(); 808 809 if (listSize > 1 && listSize < 10) { 810 padIdListWithPlaceholders(lastnResourceIds, 10); 811 } else if (listSize > 10 && listSize < 50) { 812 padIdListWithPlaceholders(lastnResourceIds, 50); 813 } else if (listSize > 50 && listSize < 100) { 814 padIdListWithPlaceholders(lastnResourceIds, 100); 815 } else if (listSize > 100 && listSize < 200) { 816 padIdListWithPlaceholders(lastnResourceIds, 200); 817 } else if (listSize > 200 && listSize < 500) { 818 padIdListWithPlaceholders(lastnResourceIds, 500); 819 } else if (listSize > 500 && listSize < 800) { 820 padIdListWithPlaceholders(lastnResourceIds, 800); 821 } 822 823 return lastnResourceIds; 824 } 825 826 private void padIdListWithPlaceholders(List<Long> theIdList, int preferredListSize) { 827 while (theIdList.size() < preferredListSize) { 828 theIdList.add(-1L); 829 } 830 } 831 832 private void createSort(QueryStack theQueryStack, SortSpec theSort, SearchParameterMap theParams) { 833 if (theSort == null || isBlank(theSort.getParamName())) { 834 return; 835 } 836 837 boolean ascending = (theSort.getOrder() == null) || (theSort.getOrder() == SortOrderEnum.ASC); 838 839 if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) { 840 841 theQueryStack.addSortOnResourceId(ascending); 842 843 } else if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) { 844 845 theQueryStack.addSortOnLastUpdated(ascending); 846 847 } else { 848 849 RuntimeSearchParam param = null; 850 851 /* 852 * If we have a sort like _sort=subject.name and we have an 853 * uplifted refchain for that combination we can do it more efficiently 854 * by using the index associated with the uplifted refchain. In this case, 855 * we need to find the actual target search parameter (corresponding 856 * to "name" in this example) so that we know what datatype it is. 857 */ 858 String paramName = theSort.getParamName(); 859 if (myStorageSettings.isIndexOnUpliftedRefchains()) { 860 String[] chains = StringUtils.split(paramName, '.'); 861 if (chains.length == 2) { 862 863 // Given: Encounter?_sort=Patient:subject.name 864 String referenceParam = chains[0]; // subject 865 String referenceParamTargetType = null; // Patient 866 String targetParam = chains[1]; // name 867 868 int colonIdx = referenceParam.indexOf(':'); 869 if (colonIdx > -1) { 870 referenceParamTargetType = referenceParam.substring(0, colonIdx); 871 referenceParam = referenceParam.substring(colonIdx + 1); 872 } 873 RuntimeSearchParam outerParam = 874 mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam); 875 if (outerParam == null) { 876 throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam); 877 } 878 879 if (outerParam.hasUpliftRefchain(targetParam)) { 880 for (String nextTargetType : outerParam.getTargets()) { 881 if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) { 882 continue; 883 } 884 RuntimeSearchParam innerParam = 885 mySearchParamRegistry.getActiveSearchParam(nextTargetType, targetParam); 886 if (innerParam != null) { 887 param = innerParam; 888 break; 889 } 890 } 891 } 892 } 893 } 894 895 int colonIdx = paramName.indexOf(':'); 896 String referenceTargetType = null; 897 if (colonIdx > -1) { 898 referenceTargetType = paramName.substring(0, colonIdx); 899 paramName = paramName.substring(colonIdx + 1); 900 } 901 902 int dotIdx = paramName.indexOf('.'); 903 String chainName = null; 904 if (param == null && dotIdx > -1) { 905 chainName = paramName.substring(dotIdx + 1); 906 paramName = paramName.substring(0, dotIdx); 907 if (chainName.contains(".")) { 908 String msg = myContext 909 .getLocalizer() 910 .getMessageSanitized( 911 BaseStorageDao.class, 912 "invalidSortParameterTooManyChains", 913 paramName + "." + chainName); 914 throw new InvalidRequestException(Msg.code(2286) + msg); 915 } 916 } 917 918 if (param == null) { 919 param = mySearchParamRegistry.getActiveSearchParam(myResourceName, paramName); 920 } 921 922 if (param == null) { 923 throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName); 924 } 925 926 if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { 927 throw new InvalidRequestException( 928 Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter"); 929 } 930 931 switch (param.getParamType()) { 932 case STRING: 933 theQueryStack.addSortOnString(myResourceName, paramName, ascending); 934 break; 935 case DATE: 936 theQueryStack.addSortOnDate(myResourceName, paramName, ascending); 937 break; 938 case REFERENCE: 939 theQueryStack.addSortOnResourceLink( 940 myResourceName, referenceTargetType, paramName, chainName, ascending); 941 break; 942 case TOKEN: 943 theQueryStack.addSortOnToken(myResourceName, paramName, ascending); 944 break; 945 case NUMBER: 946 theQueryStack.addSortOnNumber(myResourceName, paramName, ascending); 947 break; 948 case URI: 949 theQueryStack.addSortOnUri(myResourceName, paramName, ascending); 950 break; 951 case QUANTITY: 952 theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending); 953 break; 954 case COMPOSITE: 955 List<RuntimeSearchParam> compositeList = 956 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param); 957 if (compositeList == null) { 958 throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName 959 + " is not defined by the resource " + myResourceName); 960 } 961 if (compositeList.size() != 2) { 962 throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName 963 + " must have 2 composite types declared in parameter annotation, found " 964 + compositeList.size()); 965 } 966 RuntimeSearchParam left = compositeList.get(0); 967 RuntimeSearchParam right = compositeList.get(1); 968 969 createCompositeSort(theQueryStack, left.getParamType(), left.getName(), ascending); 970 createCompositeSort(theQueryStack, right.getParamType(), right.getName(), ascending); 971 972 break; 973 case SPECIAL: 974 if (LOCATION_POSITION.equals(param.getPath())) { 975 theQueryStack.addSortOnCoordsNear(paramName, ascending, theParams); 976 break; 977 } 978 throw new InvalidRequestException( 979 Msg.code(2306) + "This server does not support _sort specifications of type " 980 + param.getParamType() + " - Can't serve _sort=" + paramName); 981 982 case HAS: 983 default: 984 throw new InvalidRequestException( 985 Msg.code(1197) + "This server does not support _sort specifications of type " 986 + param.getParamType() + " - Can't serve _sort=" + paramName); 987 } 988 } 989 990 // Recurse 991 createSort(theQueryStack, theSort.getChain(), theParams); 992 } 993 994 private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) { 995 Collection<String> validSearchParameterNames = 996 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName); 997 String msg = myContext 998 .getLocalizer() 999 .getMessageSanitized( 1000 BaseStorageDao.class, 1001 "invalidSortParameter", 1002 theParamName, 1003 theResourceName, 1004 validSearchParameterNames); 1005 throw new InvalidRequestException(Msg.code(1194) + msg); 1006 } 1007 1008 private void createCompositeSort( 1009 QueryStack theQueryStack, 1010 RestSearchParameterTypeEnum theParamType, 1011 String theParamName, 1012 boolean theAscending) { 1013 1014 switch (theParamType) { 1015 case STRING: 1016 theQueryStack.addSortOnString(myResourceName, theParamName, theAscending); 1017 break; 1018 case DATE: 1019 theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending); 1020 break; 1021 case TOKEN: 1022 theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending); 1023 break; 1024 case QUANTITY: 1025 theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending); 1026 break; 1027 case NUMBER: 1028 case REFERENCE: 1029 case COMPOSITE: 1030 case URI: 1031 case HAS: 1032 case SPECIAL: 1033 default: 1034 throw new InvalidRequestException( 1035 Msg.code(1198) + "Don't know how to handle composite parameter with type of " + theParamType 1036 + " on _sort=" + theParamName); 1037 } 1038 } 1039 1040 private void doLoadPids( 1041 Collection<JpaPid> thePids, 1042 Collection<JpaPid> theIncludedPids, 1043 List<IBaseResource> theResourceListToPopulate, 1044 boolean theForHistoryOperation, 1045 Map<JpaPid, Integer> thePosition) { 1046 1047 Map<Long, Long> resourcePidToVersion = null; 1048 for (JpaPid next : thePids) { 1049 if (next.getVersion() != null && myStorageSettings.isRespectVersionsForSearchIncludes()) { 1050 if (resourcePidToVersion == null) { 1051 resourcePidToVersion = new HashMap<>(); 1052 } 1053 resourcePidToVersion.put((next).getId(), next.getVersion()); 1054 } 1055 } 1056 1057 List<Long> versionlessPids = JpaPid.toLongList(thePids); 1058 if (versionlessPids.size() < getMaximumPageSize()) { 1059 versionlessPids = normalizeIdListForLastNInClause(versionlessPids); 1060 } 1061 1062 // -- get the resource from the searchView 1063 Collection<ResourceSearchView> resourceSearchViewList = 1064 myResourceSearchViewDao.findByResourceIds(versionlessPids); 1065 1066 // -- preload all tags with tag definition if any 1067 Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList); 1068 1069 for (IBaseResourceEntity next : resourceSearchViewList) { 1070 if (next.getDeleted() != null) { 1071 continue; 1072 } 1073 1074 Class<? extends IBaseResource> resourceType = 1075 myContext.getResourceDefinition(next.getResourceType()).getImplementingClass(); 1076 1077 JpaPid resourceId = JpaPid.fromId(next.getResourceId()); 1078 1079 /* 1080 * If a specific version is requested via an include, we'll replace the current version 1081 * with the specific desired version. This is not the most efficient thing, given that 1082 * we're loading the current version and then turning around and throwing it away again. 1083 * This could be optimized and probably should be, but it's not critical given that 1084 * this only applies to includes, which don't tend to be massive in numbers. 1085 */ 1086 if (resourcePidToVersion != null) { 1087 Long version = resourcePidToVersion.get(next.getResourceId()); 1088 resourceId.setVersion(version); 1089 if (version != null && !version.equals(next.getVersion())) { 1090 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceType); 1091 next = (IBaseResourceEntity) 1092 dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null); 1093 } 1094 } 1095 1096 IBaseResource resource = null; 1097 if (next != null) { 1098 resource = myJpaStorageResourceParser.toResource( 1099 resourceType, next, tagMap.get(next.getId()), theForHistoryOperation); 1100 } 1101 if (resource == null) { 1102 ourLog.warn( 1103 "Unable to find resource {}/{}/_history/{} in database", 1104 next.getResourceType(), 1105 next.getIdDt().getIdPart(), 1106 next.getVersion()); 1107 continue; 1108 } 1109 1110 Integer index = thePosition.get(resourceId); 1111 if (index == null) { 1112 ourLog.warn("Got back unexpected resource PID {}", resourceId); 1113 continue; 1114 } 1115 1116 if (theIncludedPids.contains(resourceId)) { 1117 ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.INCLUDE); 1118 } else { 1119 ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.MATCH); 1120 } 1121 1122 theResourceListToPopulate.set(index, resource); 1123 } 1124 } 1125 1126 private Map<Long, Collection<ResourceTag>> getResourceTagMap( 1127 Collection<? extends IBaseResourceEntity> theResourceSearchViewList) { 1128 1129 List<Long> idList = new ArrayList<>(theResourceSearchViewList.size()); 1130 1131 // -- find all resource has tags 1132 for (IBaseResourceEntity resource : theResourceSearchViewList) { 1133 if (resource.isHasTags()) idList.add(resource.getId()); 1134 } 1135 1136 return getPidToTagMap(idList); 1137 } 1138 1139 @Nonnull 1140 private Map<Long, Collection<ResourceTag>> getPidToTagMap(List<Long> thePidList) { 1141 Map<Long, Collection<ResourceTag>> tagMap = new HashMap<>(); 1142 1143 // -- no tags 1144 if (thePidList.size() == 0) return tagMap; 1145 1146 // -- get all tags for the idList 1147 Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(thePidList); 1148 1149 // -- build the map, key = resourceId, value = list of ResourceTag 1150 JpaPid resourceId; 1151 Collection<ResourceTag> tagCol; 1152 for (ResourceTag tag : tagList) { 1153 1154 resourceId = JpaPid.fromId(tag.getResourceId()); 1155 tagCol = tagMap.get(resourceId.getId()); 1156 if (tagCol == null) { 1157 tagCol = new ArrayList<>(); 1158 tagCol.add(tag); 1159 tagMap.put(resourceId.getId(), tagCol); 1160 } else { 1161 tagCol.add(tag); 1162 } 1163 } 1164 1165 return tagMap; 1166 } 1167 1168 @Override 1169 public void loadResourcesByPid( 1170 Collection<JpaPid> thePids, 1171 Collection<JpaPid> theIncludedPids, 1172 List<IBaseResource> theResourceListToPopulate, 1173 boolean theForHistoryOperation, 1174 RequestDetails theDetails) { 1175 if (thePids.isEmpty()) { 1176 ourLog.debug("The include pids are empty"); 1177 // return; 1178 } 1179 1180 // Dupes will cause a crash later anyhow, but this is expensive so only do it 1181 // when running asserts 1182 assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids; 1183 1184 Map<JpaPid, Integer> position = new HashMap<>(); 1185 for (JpaPid next : thePids) { 1186 position.put(next, theResourceListToPopulate.size()); 1187 theResourceListToPopulate.add(null); 1188 } 1189 1190 // Can we fast track this loading by checking elastic search? 1191 if (isLoadingFromElasticSearchSupported(thePids)) { 1192 try { 1193 theResourceListToPopulate.addAll(loadResourcesFromElasticSearch(thePids)); 1194 return; 1195 1196 } catch (ResourceNotFoundInIndexException theE) { 1197 // some resources were not found in index, so we will inform this and resort to JPA search 1198 ourLog.warn( 1199 "Some resources were not found in index. Make sure all resources were indexed. Resorting to database search."); 1200 } 1201 } 1202 1203 // We only chunk because some jdbc drivers can't handle long param lists. 1204 new QueryChunker<JpaPid>() 1205 .chunk( 1206 thePids, 1207 t -> doLoadPids( 1208 t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position)); 1209 } 1210 1211 /** 1212 * Check if we can load the resources from Hibernate Search instead of the database. 1213 * We assume this is faster. 1214 * <p> 1215 * Hibernate Search only stores the current version, and only if enabled. 1216 * 1217 * @param thePids the pids to check for versioned references 1218 * @return can we fetch from Hibernate Search? 1219 */ 1220 private boolean isLoadingFromElasticSearchSupported(Collection<JpaPid> thePids) { 1221 // is storage enabled? 1222 return myStorageSettings.isStoreResourceInHSearchIndex() 1223 && myStorageSettings.isAdvancedHSearchIndexing() 1224 && 1225 // we don't support history 1226 thePids.stream().noneMatch(p -> p.getVersion() != null) 1227 && 1228 // skip the complexity for metadata in dstu2 1229 myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3); 1230 } 1231 1232 private List<IBaseResource> loadResourcesFromElasticSearch(Collection<JpaPid> thePids) { 1233 // Do we use the fulltextsvc via hibernate-search to load resources or be backwards compatible with older ES 1234 // only impl 1235 // to handle lastN? 1236 if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) { 1237 List<Long> pidList = thePids.stream().map(pid -> (pid).getId()).collect(Collectors.toList()); 1238 1239 List<IBaseResource> resources = myFulltextSearchSvc.getResources(pidList); 1240 return resources; 1241 } else if (!Objects.isNull(myParams) && myParams.isLastN()) { 1242 // legacy LastN implementation 1243 return myIElasticsearchSvc.getObservationResources(thePids); 1244 } else { 1245 return Collections.emptyList(); 1246 } 1247 } 1248 1249 /** 1250 * THIS SHOULD RETURN HASHSET and not just Set because we add to it later 1251 * so it can't be Collections.emptySet() or some such thing. 1252 * The JpaPid returned will have resource type populated. 1253 */ 1254 @Override 1255 public Set<JpaPid> loadIncludes( 1256 FhirContext theContext, 1257 EntityManager theEntityManager, 1258 Collection<JpaPid> theMatches, 1259 Collection<Include> theIncludes, 1260 boolean theReverseMode, 1261 DateRangeParam theLastUpdated, 1262 String theSearchIdOrDescription, 1263 RequestDetails theRequest, 1264 Integer theMaxCount) { 1265 SearchBuilderLoadIncludesParameters<JpaPid> parameters = new SearchBuilderLoadIncludesParameters<>(); 1266 parameters.setFhirContext(theContext); 1267 parameters.setEntityManager(theEntityManager); 1268 parameters.setMatches(theMatches); 1269 parameters.setIncludeFilters(theIncludes); 1270 parameters.setReverseMode(theReverseMode); 1271 parameters.setLastUpdated(theLastUpdated); 1272 parameters.setSearchIdOrDescription(theSearchIdOrDescription); 1273 parameters.setRequestDetails(theRequest); 1274 parameters.setMaxCount(theMaxCount); 1275 return loadIncludes(parameters); 1276 } 1277 1278 @Override 1279 public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theParameters) { 1280 Collection<JpaPid> matches = theParameters.getMatches(); 1281 Collection<Include> currentIncludes = theParameters.getIncludeFilters(); 1282 boolean reverseMode = theParameters.isReverseMode(); 1283 EntityManager entityManager = theParameters.getEntityManager(); 1284 Integer maxCount = theParameters.getMaxCount(); 1285 FhirContext fhirContext = theParameters.getFhirContext(); 1286 DateRangeParam lastUpdated = theParameters.getLastUpdated(); 1287 RequestDetails request = theParameters.getRequestDetails(); 1288 String searchIdOrDescription = theParameters.getSearchIdOrDescription(); 1289 List<String> desiredResourceTypes = theParameters.getDesiredResourceTypes(); 1290 boolean hasDesiredResourceTypes = desiredResourceTypes != null && !desiredResourceTypes.isEmpty(); 1291 if (CompositeInterceptorBroadcaster.hasHooks( 1292 Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, theParameters.getRequestDetails())) { 1293 CurrentThreadCaptureQueriesListener.startCapturing(); 1294 } 1295 if (matches.size() == 0) { 1296 return new HashSet<>(); 1297 } 1298 if (currentIncludes == null || currentIncludes.isEmpty()) { 1299 return new HashSet<>(); 1300 } 1301 String searchPidFieldName = reverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID; 1302 String findPidFieldName = reverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID; 1303 String findResourceTypeFieldName = reverseMode ? MY_SOURCE_RESOURCE_TYPE : MY_TARGET_RESOURCE_TYPE; 1304 String findVersionFieldName = null; 1305 if (!reverseMode && myStorageSettings.isRespectVersionsForSearchIncludes()) { 1306 findVersionFieldName = MY_TARGET_RESOURCE_VERSION; 1307 } 1308 1309 List<JpaPid> nextRoundMatches = new ArrayList<>(matches); 1310 HashSet<JpaPid> allAdded = new HashSet<>(); 1311 HashSet<JpaPid> original = new HashSet<>(matches); 1312 ArrayList<Include> includes = new ArrayList<>(currentIncludes); 1313 1314 int roundCounts = 0; 1315 StopWatch w = new StopWatch(); 1316 1317 boolean addedSomeThisRound; 1318 do { 1319 roundCounts++; 1320 1321 HashSet<JpaPid> pidsToInclude = new HashSet<>(); 1322 1323 for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) { 1324 Include nextInclude = iter.next(); 1325 if (nextInclude.isRecurse() == false) { 1326 iter.remove(); 1327 } 1328 1329 // Account for _include=* 1330 boolean matchAll = "*".equals(nextInclude.getValue()); 1331 1332 // Account for _include=[resourceType]:* 1333 String wantResourceType = null; 1334 if (!matchAll) { 1335 if ("*".equals(nextInclude.getParamName())) { 1336 wantResourceType = nextInclude.getParamType(); 1337 matchAll = true; 1338 } 1339 } 1340 1341 if (matchAll) { 1342 StringBuilder sqlBuilder = new StringBuilder(); 1343 sqlBuilder.append("SELECT r.").append(findPidFieldName); 1344 sqlBuilder.append(", r.").append(findResourceTypeFieldName); 1345 if (findVersionFieldName != null) { 1346 sqlBuilder.append(", r.").append(findVersionFieldName); 1347 } 1348 sqlBuilder.append(" FROM ResourceLink r WHERE "); 1349 1350 sqlBuilder.append("r."); 1351 sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id 1352 sqlBuilder.append(" IN (:target_pids)"); 1353 1354 /* 1355 * We need to set the resource type in 2 cases only: 1356 * 1) we are in $everything mode 1357 * (where we only want to fetch specific resource types, regardless of what is 1358 * available to fetch) 1359 * 2) we are doing revincludes 1360 * 1361 * Technically if the request is a qualified star (e.g. _include=Observation:*) we 1362 * should always be checking the source resource type on the resource link. We don't 1363 * actually index that column though by default, so in order to try and be efficient 1364 * we don't actually include it for includes (but we do for revincludes). This is 1365 * because for an include, it doesn't really make sense to include a different 1366 * resource type than the one you are searching on. 1367 */ 1368 if (wantResourceType != null 1369 && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) { 1370 // because mySourceResourceType is not part of the HFJ_RES_LINK 1371 // index, this might not be the most optimal performance. 1372 // but it is for an $everything operation (and maybe we should update the index) 1373 sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type"); 1374 } else { 1375 wantResourceType = null; 1376 } 1377 1378 // When calling $everything on a Patient instance, we don't want to recurse into new Patient 1379 // resources 1380 // (e.g. via Provenance, List, or Group) when in an $everything operation 1381 if (myParams != null 1382 && myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) { 1383 sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'"); 1384 sqlBuilder.append(" AND r.mySourceResourceType != 'Provenance'"); 1385 } 1386 if (hasDesiredResourceTypes) { 1387 sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)"); 1388 } 1389 1390 String sql = sqlBuilder.toString(); 1391 List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize()); 1392 for (Collection<JpaPid> nextPartition : partitions) { 1393 TypedQuery<?> q = entityManager.createQuery(sql, Object[].class); 1394 q.setParameter("target_pids", JpaPid.toLongList(nextPartition)); 1395 if (wantResourceType != null) { 1396 q.setParameter("want_resource_type", wantResourceType); 1397 } 1398 if (maxCount != null) { 1399 q.setMaxResults(maxCount); 1400 } 1401 if (hasDesiredResourceTypes) { 1402 q.setParameter("desired_target_resource_types", String.join(", ", desiredResourceTypes)); 1403 } 1404 List<?> results = q.getResultList(); 1405 for (Object nextRow : results) { 1406 if (nextRow == null) { 1407 // This can happen if there are outgoing references which are canonical or point to 1408 // other servers 1409 continue; 1410 } 1411 1412 Long version = null; 1413 Long resourceLink = (Long) ((Object[]) nextRow)[0]; 1414 String resourceType = (String) ((Object[]) nextRow)[1]; 1415 if (findVersionFieldName != null) { 1416 version = (Long) ((Object[]) nextRow)[2]; 1417 } 1418 1419 if (resourceLink != null) { 1420 JpaPid pid = 1421 JpaPid.fromIdAndVersionAndResourceType(resourceLink, version, resourceType); 1422 pidsToInclude.add(pid); 1423 } 1424 } 1425 } 1426 } else { 1427 List<String> paths; 1428 1429 // Start replace 1430 RuntimeSearchParam param; 1431 String resType = nextInclude.getParamType(); 1432 if (isBlank(resType)) { 1433 continue; 1434 } 1435 RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType); 1436 if (def == null) { 1437 ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); 1438 continue; 1439 } 1440 1441 String paramName = nextInclude.getParamName(); 1442 if (isNotBlank(paramName)) { 1443 param = mySearchParamRegistry.getActiveSearchParam(resType, paramName); 1444 } else { 1445 param = null; 1446 } 1447 if (param == null) { 1448 ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); 1449 continue; 1450 } 1451 1452 paths = param.getPathsSplitForResourceType(resType); 1453 // end replace 1454 1455 Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param); 1456 1457 for (String nextPath : paths) { 1458 String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) 1459 ? "src_resource_id" 1460 : "target_resource_id"; 1461 String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS; 1462 if (findVersionFieldName != null) { 1463 fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS; 1464 } 1465 1466 // Query for includes lookup has 2 cases 1467 // Case 1: Where target_resource_id is available in hfj_res_link table for local references 1468 // Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical 1469 // url in target_resource_url 1470 1471 // Case 1: 1472 Map<String, Object> localReferenceQueryParams = new HashMap<>(); 1473 1474 String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) 1475 ? "target_resource_id" 1476 : "src_resource_id"; 1477 StringBuilder localReferenceQuery = 1478 new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r " 1479 + " WHERE r.src_path = :src_path AND " 1480 + " r.target_resource_id IS NOT NULL AND " 1481 + " r." 1482 + searchPidFieldSqlColumn + " IN (:target_pids) "); 1483 localReferenceQueryParams.put("src_path", nextPath); 1484 // we loop over target_pids later. 1485 if (targetResourceTypes != null) { 1486 if (targetResourceTypes.size() == 1) { 1487 localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type "); 1488 localReferenceQueryParams.put( 1489 "target_resource_type", 1490 targetResourceTypes.iterator().next()); 1491 } else { 1492 localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) "); 1493 localReferenceQueryParams.put("target_resource_types", targetResourceTypes); 1494 } 1495 } 1496 1497 // Case 2: 1498 Pair<String, Map<String, Object>> canonicalQuery = buildCanonicalUrlQuery( 1499 findVersionFieldName, searchPidFieldSqlColumn, targetResourceTypes); 1500 1501 // @formatter:on 1502 1503 String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft(); 1504 1505 List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize()); 1506 for (Collection<JpaPid> nextPartition : partitions) { 1507 Query q = entityManager.createNativeQuery(sql, Tuple.class); 1508 q.setParameter("target_pids", JpaPid.toLongList(nextPartition)); 1509 localReferenceQueryParams.forEach(q::setParameter); 1510 canonicalQuery.getRight().forEach(q::setParameter); 1511 1512 if (maxCount != null) { 1513 q.setMaxResults(maxCount); 1514 } 1515 @SuppressWarnings("unchecked") 1516 List<Tuple> results = q.getResultList(); 1517 for (Tuple result : results) { 1518 if (result != null) { 1519 Long resourceId = 1520 NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS))); 1521 Long resourceVersion = null; 1522 if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) { 1523 resourceVersion = NumberUtils.createLong( 1524 String.valueOf(result.get(RESOURCE_VERSION_ALIAS))); 1525 } 1526 pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion)); 1527 } 1528 } 1529 } 1530 } 1531 } 1532 } 1533 1534 nextRoundMatches.clear(); 1535 for (JpaPid next : pidsToInclude) { 1536 if (!original.contains(next) && !allAdded.contains(next)) { 1537 nextRoundMatches.add(next); 1538 } 1539 } 1540 1541 addedSomeThisRound = allAdded.addAll(pidsToInclude); 1542 1543 if (maxCount != null && allAdded.size() >= maxCount) { 1544 break; 1545 } 1546 1547 } while (!includes.isEmpty() && !nextRoundMatches.isEmpty() && addedSomeThisRound); 1548 1549 allAdded.removeAll(original); 1550 1551 ourLog.info( 1552 "Loaded {} {} in {} rounds and {} ms for search {}", 1553 allAdded.size(), 1554 reverseMode ? "_revincludes" : "_includes", 1555 roundCounts, 1556 w.getMillisAndRestart(), 1557 searchIdOrDescription); 1558 1559 if (CompositeInterceptorBroadcaster.hasHooks( 1560 Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, request)) { 1561 callRawSqlHookWithCurrentThreadQueries(request); 1562 } 1563 // Interceptor call: STORAGE_PREACCESS_RESOURCES 1564 // This can be used to remove results from the search result details before 1565 // the user has a chance to know that they were in the results 1566 if (!allAdded.isEmpty()) { 1567 1568 if (CompositeInterceptorBroadcaster.hasHooks( 1569 Pointcut.STORAGE_PREACCESS_RESOURCES, myInterceptorBroadcaster, request)) { 1570 List<JpaPid> includedPidList = new ArrayList<>(allAdded); 1571 JpaPreResourceAccessDetails accessDetails = 1572 new JpaPreResourceAccessDetails(includedPidList, () -> this); 1573 HookParams params = new HookParams() 1574 .add(IPreResourceAccessDetails.class, accessDetails) 1575 .add(RequestDetails.class, request) 1576 .addIfMatchesType(ServletRequestDetails.class, request); 1577 CompositeInterceptorBroadcaster.doCallHooks( 1578 myInterceptorBroadcaster, request, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 1579 1580 for (int i = includedPidList.size() - 1; i >= 0; i--) { 1581 if (accessDetails.isDontReturnResourceAtIndex(i)) { 1582 JpaPid value = includedPidList.remove(i); 1583 if (value != null) { 1584 allAdded.remove(value); 1585 } 1586 } 1587 } 1588 } 1589 } 1590 1591 return allAdded; 1592 } 1593 1594 /** 1595 * Given a 1596 * @param request 1597 */ 1598 private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) { 1599 SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing(); 1600 HookParams params = new HookParams() 1601 .add(RequestDetails.class, request) 1602 .addIfMatchesType(ServletRequestDetails.class, request) 1603 .add(SqlQueryList.class, capturedQueries); 1604 CompositeInterceptorBroadcaster.doCallHooks( 1605 myInterceptorBroadcaster, request, Pointcut.JPA_PERFTRACE_RAW_SQL, params); 1606 } 1607 1608 @Nullable 1609 private static Set<String> computeTargetResourceTypes(Include nextInclude, RuntimeSearchParam param) { 1610 String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); 1611 boolean haveTargetTypesDefinedByParam = param.hasTargets(); 1612 Set<String> targetResourceTypes; 1613 if (targetResourceType != null) { 1614 targetResourceTypes = Set.of(targetResourceType); 1615 } else if (haveTargetTypesDefinedByParam) { 1616 targetResourceTypes = param.getTargets(); 1617 } else { 1618 // all types! 1619 targetResourceTypes = null; 1620 } 1621 return targetResourceTypes; 1622 } 1623 1624 @Nonnull 1625 private Pair<String, Map<String, Object>> buildCanonicalUrlQuery( 1626 String theVersionFieldName, String thePidFieldSqlColumn, Set<String> theTargetResourceTypes) { 1627 String fieldsToLoadFromSpidxUriTable = "rUri.res_id"; 1628 if (theVersionFieldName != null) { 1629 // canonical-uri references aren't versioned, but we need to match the column count for the UNION 1630 fieldsToLoadFromSpidxUriTable += ", NULL"; 1631 } 1632 // The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url. 1633 // But sp_name isn't indexed, so we use hash_identity instead. 1634 if (theTargetResourceTypes == null) { 1635 // hash_identity includes the resource type. So a null wildcard must be replaced with a list of all types. 1636 theTargetResourceTypes = myDaoRegistry.getRegisteredDaoTypes(); 1637 } 1638 assert !theTargetResourceTypes.isEmpty(); 1639 1640 Set<Long> identityHashesForTypes = theTargetResourceTypes.stream() 1641 .map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity( 1642 myPartitionSettings, myRequestPartitionId, type, "url")) 1643 .collect(Collectors.toSet()); 1644 1645 Map<String, Object> canonicalUriQueryParams = new HashMap<>(); 1646 StringBuilder canonicalUrlQuery = new StringBuilder( 1647 "SELECT " + fieldsToLoadFromSpidxUriTable + " FROM hfj_res_link r " + " JOIN hfj_spidx_uri rUri ON ( "); 1648 // join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2 1649 if (theTargetResourceTypes.size() == 1) { 1650 canonicalUrlQuery.append(" rUri.hash_identity = :uri_identity_hash "); 1651 canonicalUriQueryParams.put( 1652 "uri_identity_hash", identityHashesForTypes.iterator().next()); 1653 } else { 1654 canonicalUrlQuery.append(" rUri.hash_identity in (:uri_identity_hashes) "); 1655 canonicalUriQueryParams.put("uri_identity_hashes", identityHashesForTypes); 1656 } 1657 1658 canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )" + " WHERE r.src_path = :src_path AND " 1659 + " r.target_resource_id IS NULL AND " 1660 + " r." 1661 + thePidFieldSqlColumn + " IN (:target_pids) "); 1662 return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams); 1663 } 1664 1665 private List<Collection<JpaPid>> partition(Collection<JpaPid> theNextRoundMatches, int theMaxLoad) { 1666 if (theNextRoundMatches.size() <= theMaxLoad) { 1667 return Collections.singletonList(theNextRoundMatches); 1668 } else { 1669 1670 List<Collection<JpaPid>> retVal = new ArrayList<>(); 1671 Collection<JpaPid> current = null; 1672 for (JpaPid next : theNextRoundMatches) { 1673 if (current == null) { 1674 current = new ArrayList<>(theMaxLoad); 1675 retVal.add(current); 1676 } 1677 1678 current.add(next); 1679 1680 if (current.size() >= theMaxLoad) { 1681 current = null; 1682 } 1683 } 1684 1685 return retVal; 1686 } 1687 } 1688 1689 private void attemptComboUniqueSpProcessing( 1690 QueryStack theQueryStack3, @Nonnull SearchParameterMap theParams, RequestDetails theRequest) { 1691 RuntimeSearchParam comboParam = null; 1692 List<String> comboParamNames = null; 1693 List<RuntimeSearchParam> exactMatchParams = 1694 mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet()); 1695 if (exactMatchParams.size() > 0) { 1696 comboParam = exactMatchParams.get(0); 1697 comboParamNames = new ArrayList<>(theParams.keySet()); 1698 } 1699 1700 if (comboParam == null) { 1701 List<RuntimeSearchParam> candidateComboParams = 1702 mySearchParamRegistry.getActiveComboSearchParams(myResourceName); 1703 for (RuntimeSearchParam nextCandidate : candidateComboParams) { 1704 List<String> nextCandidateParamNames = 1705 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream() 1706 .map(t -> t.getName()) 1707 .collect(Collectors.toList()); 1708 if (theParams.keySet().containsAll(nextCandidateParamNames)) { 1709 comboParam = nextCandidate; 1710 comboParamNames = nextCandidateParamNames; 1711 break; 1712 } 1713 } 1714 } 1715 1716 if (comboParam != null) { 1717 // Since we're going to remove elements below 1718 theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList)); 1719 1720 StringBuilder sb = new StringBuilder(); 1721 sb.append(myResourceName); 1722 sb.append("?"); 1723 1724 boolean first = true; 1725 1726 Collections.sort(comboParamNames); 1727 for (String nextParamName : comboParamNames) { 1728 List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName); 1729 1730 // TODO Hack to fix weird IOOB on the next stanza until James comes back and makes sense of this. 1731 if (nextValues.isEmpty()) { 1732 ourLog.error( 1733 "query parameter {} is unexpectedly empty. Encountered while considering {} index for {}", 1734 nextParamName, 1735 comboParam.getName(), 1736 theRequest.getCompleteUrl()); 1737 sb = null; 1738 break; 1739 } 1740 1741 if (nextValues.get(0).size() != 1) { 1742 sb = null; 1743 break; 1744 } 1745 1746 // Reference params are only eligible for using a composite index if they 1747 // are qualified 1748 RuntimeSearchParam nextParamDef = 1749 mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName); 1750 if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 1751 ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0); 1752 if (isBlank(param.getResourceType())) { 1753 sb = null; 1754 break; 1755 } 1756 } 1757 1758 List<? extends IQueryParameterType> nextAnd = nextValues.remove(0); 1759 IQueryParameterType nextOr = nextAnd.remove(0); 1760 String nextOrValue = nextOr.getValueAsQueryToken(myContext); 1761 1762 if (comboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) { 1763 if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) { 1764 nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue); 1765 } 1766 } 1767 1768 if (first) { 1769 first = false; 1770 } else { 1771 sb.append('&'); 1772 } 1773 1774 nextParamName = UrlUtil.escapeUrlParam(nextParamName); 1775 nextOrValue = UrlUtil.escapeUrlParam(nextOrValue); 1776 1777 sb.append(nextParamName).append('=').append(nextOrValue); 1778 } 1779 1780 if (sb != null) { 1781 String indexString = sb.toString(); 1782 ourLog.debug( 1783 "Checking for {} combo index for query: {}", comboParam.getComboSearchParamType(), indexString); 1784 1785 // Interceptor broadcast: JPA_PERFTRACE_INFO 1786 StorageProcessingMessage msg = new StorageProcessingMessage() 1787 .setMessage("Using " + comboParam.getComboSearchParamType() + " index for query for search: " 1788 + indexString); 1789 HookParams params = new HookParams() 1790 .add(RequestDetails.class, theRequest) 1791 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1792 .add(StorageProcessingMessage.class, msg); 1793 CompositeInterceptorBroadcaster.doCallHooks( 1794 myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); 1795 1796 switch (comboParam.getComboSearchParamType()) { 1797 case UNIQUE: 1798 theQueryStack3.addPredicateCompositeUnique(indexString, myRequestPartitionId); 1799 break; 1800 case NON_UNIQUE: 1801 theQueryStack3.addPredicateCompositeNonUnique(indexString, myRequestPartitionId); 1802 break; 1803 } 1804 1805 // Remove any empty parameters remaining after this 1806 theParams.clean(); 1807 } 1808 } 1809 } 1810 1811 private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) { 1812 for (int i = 0; i < theListOfLists.size(); i++) { 1813 List<T> oldSubList = theListOfLists.get(i); 1814 if (!(oldSubList instanceof ArrayList)) { 1815 List<T> newSubList = new ArrayList<>(oldSubList); 1816 theListOfLists.set(i, newSubList); 1817 } 1818 } 1819 } 1820 1821 @Override 1822 public void setFetchSize(int theFetchSize) { 1823 myFetchSize = theFetchSize; 1824 } 1825 1826 public SearchParameterMap getParams() { 1827 return myParams; 1828 } 1829 1830 public CriteriaBuilder getBuilder() { 1831 return myCriteriaBuilder; 1832 } 1833 1834 public Class<? extends IBaseResource> getResourceType() { 1835 return myResourceType; 1836 } 1837 1838 public String getResourceName() { 1839 return myResourceName; 1840 } 1841 1842 /** 1843 * IncludesIterator, used to recursively fetch resources from the provided list of PIDs 1844 */ 1845 public class IncludesIterator extends BaseIterator<JpaPid> implements Iterator<JpaPid> { 1846 1847 private final RequestDetails myRequest; 1848 private final Set<JpaPid> myCurrentPids; 1849 private Iterator<JpaPid> myCurrentIterator; 1850 private JpaPid myNext; 1851 1852 IncludesIterator(Set<JpaPid> thePidSet, RequestDetails theRequest) { 1853 myCurrentPids = new HashSet<>(thePidSet); 1854 myCurrentIterator = null; 1855 myRequest = theRequest; 1856 } 1857 1858 private void fetchNext() { 1859 while (myNext == null) { 1860 1861 if (myCurrentIterator == null) { 1862 Set<Include> includes = new HashSet<>(); 1863 if (myParams.containsKey(Constants.PARAM_TYPE)) { 1864 for (List<IQueryParameterType> typeList : myParams.get(Constants.PARAM_TYPE)) { 1865 for (IQueryParameterType type : typeList) { 1866 String queryString = ParameterUtil.unescape(type.getValueAsQueryToken(myContext)); 1867 for (String resourceType : queryString.split(",")) { 1868 String rt = resourceType.trim(); 1869 if (isNotBlank(rt)) { 1870 includes.add(new Include(rt + ":*", true)); 1871 } 1872 } 1873 } 1874 } 1875 } 1876 if (includes.isEmpty()) { 1877 includes.add(new Include("*", true)); 1878 } 1879 Set<JpaPid> newPids = loadIncludes( 1880 myContext, 1881 myEntityManager, 1882 myCurrentPids, 1883 includes, 1884 false, 1885 getParams().getLastUpdated(), 1886 mySearchUuid, 1887 myRequest, 1888 null); 1889 myCurrentIterator = newPids.iterator(); 1890 } 1891 1892 if (myCurrentIterator.hasNext()) { 1893 myNext = myCurrentIterator.next(); 1894 } else { 1895 myNext = NO_MORE; 1896 } 1897 } 1898 } 1899 1900 @Override 1901 public boolean hasNext() { 1902 fetchNext(); 1903 return !NO_MORE.equals(myNext); 1904 } 1905 1906 @Override 1907 public JpaPid next() { 1908 fetchNext(); 1909 JpaPid retVal = myNext; 1910 myNext = null; 1911 return retVal; 1912 } 1913 } 1914 1915 /** 1916 * Basic Query iterator, used to fetch the results of a query. 1917 */ 1918 private final class QueryIterator extends BaseIterator<JpaPid> implements IResultIterator<JpaPid> { 1919 1920 private final SearchRuntimeDetails mySearchRuntimeDetails; 1921 private final RequestDetails myRequest; 1922 private final boolean myHaveRawSqlHooks; 1923 private final boolean myHavePerfTraceFoundIdHook; 1924 private final SortSpec mySort; 1925 private final Integer myOffset; 1926 private boolean myFirst = true; 1927 private IncludesIterator myIncludesIterator; 1928 private JpaPid myNext; 1929 private ISearchQueryExecutor myResultsIterator; 1930 private boolean myFetchIncludesForEverythingOperation; 1931 private int mySkipCount = 0; 1932 private int myNonSkipCount = 0; 1933 private List<ISearchQueryExecutor> myQueryList = new ArrayList<>(); 1934 1935 private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) { 1936 mySearchRuntimeDetails = theSearchRuntimeDetails; 1937 mySort = myParams.getSort(); 1938 myOffset = myParams.getOffset(); 1939 myRequest = theRequest; 1940 1941 // everything requires fetching recursively all related resources 1942 if (myParams.getEverythingMode() != null) { 1943 myFetchIncludesForEverythingOperation = true; 1944 } 1945 1946 myHavePerfTraceFoundIdHook = CompositeInterceptorBroadcaster.hasHooks( 1947 Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest); 1948 myHaveRawSqlHooks = CompositeInterceptorBroadcaster.hasHooks( 1949 Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest); 1950 } 1951 1952 private void fetchNext() { 1953 try { 1954 if (myHaveRawSqlHooks) { 1955 CurrentThreadCaptureQueriesListener.startCapturing(); 1956 } 1957 1958 // If we don't have a query yet, create one 1959 if (myResultsIterator == null) { 1960 if (myMaxResultsToFetch == null) { 1961 if (myParams.getLoadSynchronousUpTo() != null) { 1962 myMaxResultsToFetch = myParams.getLoadSynchronousUpTo(); 1963 } else if (myParams.getOffset() != null && myParams.getCount() != null) { 1964 myMaxResultsToFetch = myParams.getCount(); 1965 } else { 1966 myMaxResultsToFetch = myStorageSettings.getFetchSizeDefaultMaximum(); 1967 } 1968 } 1969 1970 // assigns the results iterator 1971 initializeIteratorQuery(myOffset, myMaxResultsToFetch); 1972 1973 if (myAlsoIncludePids == null) { 1974 myAlsoIncludePids = new ArrayList<>(); 1975 } 1976 } 1977 1978 if (myNext == null) { 1979 for (Iterator<JpaPid> myPreResultsIterator = myAlsoIncludePids.iterator(); 1980 myPreResultsIterator.hasNext(); ) { 1981 JpaPid next = myPreResultsIterator.next(); 1982 if (next != null) 1983 if (myPidSet.add(next)) { 1984 myNext = next; 1985 break; 1986 } 1987 } 1988 1989 if (myNext == null) { 1990 while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) { 1991 // Update iterator with next chunk if necessary. 1992 if (!myResultsIterator.hasNext()) { 1993 retrieveNextIteratorQuery(); 1994 } 1995 1996 Long nextLong = myResultsIterator.next(); 1997 if (myHavePerfTraceFoundIdHook) { 1998 HookParams params = new HookParams() 1999 .add(Integer.class, System.identityHashCode(this)) 2000 .add(Object.class, nextLong); 2001 CompositeInterceptorBroadcaster.doCallHooks( 2002 myInterceptorBroadcaster, 2003 myRequest, 2004 Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, 2005 params); 2006 } 2007 2008 if (nextLong != null) { 2009 JpaPid next = JpaPid.fromId(nextLong); 2010 if (myPidSet.add(next)) { 2011 myNext = next; 2012 myNonSkipCount++; 2013 break; 2014 } else { 2015 mySkipCount++; 2016 } 2017 } 2018 2019 if (!myResultsIterator.hasNext()) { 2020 if (myMaxResultsToFetch != null 2021 && (mySkipCount + myNonSkipCount == myMaxResultsToFetch)) { 2022 if (mySkipCount > 0 && myNonSkipCount == 0) { 2023 2024 StorageProcessingMessage message = new StorageProcessingMessage(); 2025 String msg = "Pass completed with no matching results seeking rows " 2026 + myPidSet.size() + "-" + mySkipCount 2027 + ". This indicates an inefficient query! Retrying with new max count of " 2028 + myMaxResultsToFetch; 2029 ourLog.warn(msg); 2030 message.setMessage(msg); 2031 HookParams params = new HookParams() 2032 .add(RequestDetails.class, myRequest) 2033 .addIfMatchesType(ServletRequestDetails.class, myRequest) 2034 .add(StorageProcessingMessage.class, message); 2035 CompositeInterceptorBroadcaster.doCallHooks( 2036 myInterceptorBroadcaster, 2037 myRequest, 2038 Pointcut.JPA_PERFTRACE_WARNING, 2039 params); 2040 2041 myMaxResultsToFetch += 1000; 2042 initializeIteratorQuery(myOffset, myMaxResultsToFetch); 2043 } 2044 } 2045 } 2046 } 2047 } 2048 2049 if (myNext == null) { 2050 // if we got here, it means the current PjaPid has already been processed 2051 // and we will decide (here) if we need to fetch related resources recursively 2052 if (myFetchIncludesForEverythingOperation) { 2053 myIncludesIterator = new IncludesIterator(myPidSet, myRequest); 2054 myFetchIncludesForEverythingOperation = false; 2055 } 2056 if (myIncludesIterator != null) { 2057 while (myIncludesIterator.hasNext()) { 2058 JpaPid next = myIncludesIterator.next(); 2059 if (next != null) 2060 if (myPidSet.add(next)) { 2061 myNext = next; 2062 break; 2063 } 2064 } 2065 if (myNext == null) { 2066 myNext = NO_MORE; 2067 } 2068 } else { 2069 myNext = NO_MORE; 2070 } 2071 } 2072 } // if we need to fetch the next result 2073 2074 mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size()); 2075 2076 } finally { 2077 // search finished - fire hooks 2078 if (myHaveRawSqlHooks) { 2079 callRawSqlHookWithCurrentThreadQueries(myRequest); 2080 } 2081 } 2082 2083 if (myFirst) { 2084 HookParams params = new HookParams() 2085 .add(RequestDetails.class, myRequest) 2086 .addIfMatchesType(ServletRequestDetails.class, myRequest) 2087 .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); 2088 CompositeInterceptorBroadcaster.doCallHooks( 2089 myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params); 2090 myFirst = false; 2091 } 2092 2093 if (NO_MORE.equals(myNext)) { 2094 HookParams params = new HookParams() 2095 .add(RequestDetails.class, myRequest) 2096 .addIfMatchesType(ServletRequestDetails.class, myRequest) 2097 .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); 2098 CompositeInterceptorBroadcaster.doCallHooks( 2099 myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params); 2100 } 2101 } 2102 2103 private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) { 2104 if (myQueryList.isEmpty()) { 2105 // Capture times for Lucene/Elasticsearch queries as well 2106 mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); 2107 myQueryList = createQuery( 2108 myParams, mySort, theOffset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails); 2109 } 2110 2111 mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); 2112 2113 retrieveNextIteratorQuery(); 2114 2115 mySkipCount = 0; 2116 myNonSkipCount = 0; 2117 } 2118 2119 private void retrieveNextIteratorQuery() { 2120 close(); 2121 if (myQueryList != null && myQueryList.size() > 0) { 2122 myResultsIterator = myQueryList.remove(0); 2123 myHasNextIteratorQuery = true; 2124 } else { 2125 myResultsIterator = SearchQueryExecutor.emptyExecutor(); 2126 myHasNextIteratorQuery = false; 2127 } 2128 } 2129 2130 @Override 2131 public boolean hasNext() { 2132 if (myNext == null) { 2133 fetchNext(); 2134 } 2135 return !NO_MORE.equals(myNext); 2136 } 2137 2138 @Override 2139 public JpaPid next() { 2140 fetchNext(); 2141 JpaPid retVal = myNext; 2142 myNext = null; 2143 Validate.isTrue(!NO_MORE.equals(retVal), "No more elements"); 2144 return retVal; 2145 } 2146 2147 @Override 2148 public int getSkippedCount() { 2149 return mySkipCount; 2150 } 2151 2152 @Override 2153 public int getNonSkippedCount() { 2154 return myNonSkipCount; 2155 } 2156 2157 @Override 2158 public Collection<JpaPid> getNextResultBatch(long theBatchSize) { 2159 Collection<JpaPid> batch = new ArrayList<>(); 2160 while (this.hasNext() && batch.size() < theBatchSize) { 2161 batch.add(this.next()); 2162 } 2163 return batch; 2164 } 2165 2166 @Override 2167 public void close() { 2168 if (myResultsIterator != null) { 2169 myResultsIterator.close(); 2170 } 2171 myResultsIterator = null; 2172 } 2173 } 2174 2175 public static int getMaximumPageSize() { 2176 if (myUseMaxPageSize50ForTest) { 2177 return MAXIMUM_PAGE_SIZE_FOR_TESTING; 2178 } else { 2179 return MAXIMUM_PAGE_SIZE; 2180 } 2181 } 2182 2183 public static void setMaxPageSize50ForTest(boolean theIsTest) { 2184 myUseMaxPageSize50ForTest = theIsTest; 2185 } 2186}