001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.builder; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 024import ca.uhn.fhir.context.ComboSearchParamType; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.RuntimeResourceDefinition; 028import ca.uhn.fhir.context.RuntimeSearchParam; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.interceptor.api.HookParams; 031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 032import ca.uhn.fhir.interceptor.api.Pointcut; 033import ca.uhn.fhir.interceptor.model.RequestPartitionId; 034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 035import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 036import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 037import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 038import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; 039import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; 040import ca.uhn.fhir.jpa.dao.BaseStorageDao; 041import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; 042import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 043import ca.uhn.fhir.jpa.dao.IResultIterator; 044import ca.uhn.fhir.jpa.dao.ISearchBuilder; 045import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; 046import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao; 047import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; 048import ca.uhn.fhir.jpa.dao.search.ResourceNotFoundInIndexException; 049import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails; 050import ca.uhn.fhir.jpa.model.config.PartitionSettings; 051import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 052import ca.uhn.fhir.jpa.model.dao.JpaPid; 053import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; 054import ca.uhn.fhir.jpa.model.entity.BaseTag; 055import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 056import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; 057import ca.uhn.fhir.jpa.model.entity.ResourceTag; 058import ca.uhn.fhir.jpa.model.search.SearchBuilderLoadIncludesParameters; 059import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; 060import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 061import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 062import ca.uhn.fhir.jpa.search.SearchConstants; 063import ca.uhn.fhir.jpa.search.builder.models.ResolvedSearchQueryExecutor; 064import ca.uhn.fhir.jpa.search.builder.sql.GeneratedSql; 065import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder; 066import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryExecutor; 067import ca.uhn.fhir.jpa.search.builder.sql.SqlObjectFactory; 068import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc; 069import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 070import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper; 071import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 072import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper; 073import ca.uhn.fhir.jpa.util.BaseIterator; 074import ca.uhn.fhir.jpa.util.CartesianProductUtil; 075import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; 076import ca.uhn.fhir.jpa.util.QueryChunker; 077import ca.uhn.fhir.jpa.util.SqlQueryList; 078import ca.uhn.fhir.model.api.IQueryParameterType; 079import ca.uhn.fhir.model.api.Include; 080import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 081import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 082import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; 083import ca.uhn.fhir.rest.api.Constants; 084import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 085import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 086import ca.uhn.fhir.rest.api.SortOrderEnum; 087import ca.uhn.fhir.rest.api.SortSpec; 088import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 089import ca.uhn.fhir.rest.api.server.RequestDetails; 090import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 091import ca.uhn.fhir.rest.param.BaseParamWithPrefix; 092import ca.uhn.fhir.rest.param.DateParam; 093import ca.uhn.fhir.rest.param.DateRangeParam; 094import ca.uhn.fhir.rest.param.ParameterUtil; 095import ca.uhn.fhir.rest.param.ReferenceParam; 096import ca.uhn.fhir.rest.param.StringParam; 097import ca.uhn.fhir.rest.param.TokenParam; 098import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 099import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 100import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 101import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 102import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 103import ca.uhn.fhir.system.HapiSystemProperties; 104import ca.uhn.fhir.util.StopWatch; 105import ca.uhn.fhir.util.StringUtil; 106import ca.uhn.fhir.util.UrlUtil; 107import com.google.common.annotations.VisibleForTesting; 108import com.google.common.collect.ListMultimap; 109import com.google.common.collect.Lists; 110import com.google.common.collect.MultimapBuilder; 111import com.healthmarketscience.sqlbuilder.Condition; 112import jakarta.annotation.Nonnull; 113import jakarta.annotation.Nullable; 114import jakarta.persistence.EntityManager; 115import jakarta.persistence.PersistenceContext; 116import jakarta.persistence.PersistenceContextType; 117import jakarta.persistence.Query; 118import jakarta.persistence.Tuple; 119import jakarta.persistence.TypedQuery; 120import jakarta.persistence.criteria.CriteriaBuilder; 121import org.apache.commons.collections4.ListUtils; 122import org.apache.commons.lang3.StringUtils; 123import org.apache.commons.lang3.Validate; 124import org.apache.commons.lang3.math.NumberUtils; 125import org.apache.commons.lang3.tuple.Pair; 126import org.hl7.fhir.instance.model.api.IAnyResource; 127import org.hl7.fhir.instance.model.api.IBaseResource; 128import org.hl7.fhir.instance.model.api.IIdType; 129import org.slf4j.Logger; 130import org.slf4j.LoggerFactory; 131import org.springframework.beans.factory.annotation.Autowired; 132import org.springframework.jdbc.core.JdbcTemplate; 133import org.springframework.transaction.support.TransactionSynchronizationManager; 134 135import java.util.ArrayList; 136import java.util.Collection; 137import java.util.Collections; 138import java.util.Comparator; 139import java.util.HashMap; 140import java.util.HashSet; 141import java.util.Iterator; 142import java.util.LinkedList; 143import java.util.List; 144import java.util.Map; 145import java.util.Objects; 146import java.util.Set; 147import java.util.stream.Collectors; 148 149import static ca.uhn.fhir.jpa.model.util.JpaConstants.UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE; 150import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION; 151import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with; 152import static ca.uhn.fhir.jpa.util.InClauseNormalizer.normalizeIdListForInClause; 153import static java.util.Objects.requireNonNull; 154import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; 155import static org.apache.commons.lang3.StringUtils.defaultString; 156import static org.apache.commons.lang3.StringUtils.isBlank; 157import static org.apache.commons.lang3.StringUtils.isNotBlank; 158 159/** 160 * The SearchBuilder is responsible for actually forming the SQL query that handles 161 * searches for resources 162 */ 163public class SearchBuilder implements ISearchBuilder<JpaPid> { 164 165 /** 166 * See loadResourcesByPid 167 * for an explanation of why we use the constant 800 168 */ 169 // NB: keep public 170 @Deprecated 171 public static final int MAXIMUM_PAGE_SIZE = SearchConstants.MAX_PAGE_SIZE; 172 173 public static final String RESOURCE_ID_ALIAS = "resource_id"; 174 public static final String PARTITION_ID_ALIAS = "partition_id"; 175 public static final String RESOURCE_VERSION_ALIAS = "resource_version"; 176 private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class); 177 private static final JpaPid NO_MORE = JpaPid.fromId(-1L); 178 private static final String MY_SOURCE_RESOURCE_PID = "mySourceResourcePid"; 179 private static final String MY_SOURCE_RESOURCE_PARTITION_ID = "myPartitionIdValue"; 180 private static final String MY_SOURCE_RESOURCE_TYPE = "mySourceResourceType"; 181 private static final String MY_TARGET_RESOURCE_PID = "myTargetResourcePid"; 182 private static final String MY_TARGET_RESOURCE_PARTITION_ID = "myTargetResourcePartitionId"; 183 private static final String MY_TARGET_RESOURCE_TYPE = "myTargetResourceType"; 184 private static final String MY_TARGET_RESOURCE_VERSION = "myTargetResourceVersion"; 185 public static final JpaPid[] EMPTY_JPA_PID_ARRAY = new JpaPid[0]; 186 public static boolean myUseMaxPageSize50ForTest = false; 187 public static Integer myMaxPageSizeForTests = null; 188 protected final IInterceptorBroadcaster myInterceptorBroadcaster; 189 protected final IResourceTagDao myResourceTagDao; 190 private String myResourceName; 191 private final Class<? extends IBaseResource> myResourceType; 192 private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory; 193 private final SqlObjectFactory mySqlBuilderFactory; 194 private final HibernatePropertiesProvider myDialectProvider; 195 private final ISearchParamRegistry mySearchParamRegistry; 196 private final PartitionSettings myPartitionSettings; 197 private final DaoRegistry myDaoRegistry; 198 private final FhirContext myContext; 199 private final IIdHelperService<JpaPid> myIdHelperService; 200 private final JpaStorageSettings myStorageSettings; 201 202 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 203 protected EntityManager myEntityManager; 204 205 private CriteriaBuilder myCriteriaBuilder; 206 private SearchParameterMap myParams; 207 private String mySearchUuid; 208 private int myFetchSize; 209 private Integer myMaxResultsToFetch; 210 private Set<JpaPid> myPidSet; 211 private boolean myHasNextIteratorQuery = false; 212 private RequestPartitionId myRequestPartitionId; 213 214 @Autowired(required = false) 215 private IFulltextSearchSvc myFulltextSearchSvc; 216 217 @Autowired(required = false) 218 private IElasticsearchSvc myIElasticsearchSvc; 219 220 @Autowired 221 private IJpaStorageResourceParser myJpaStorageResourceParser; 222 223 @Autowired 224 private IResourceHistoryTableDao myResourceHistoryTableDao; 225 226 @Autowired 227 private IResourceHistoryTagDao myResourceHistoryTagDao; 228 229 @Autowired 230 private IRequestPartitionHelperSvc myPartitionHelperSvc; 231 232 /** 233 * Constructor 234 */ 235 @SuppressWarnings({"rawtypes", "unchecked"}) 236 public SearchBuilder( 237 String theResourceName, 238 JpaStorageSettings theStorageSettings, 239 HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory, 240 SqlObjectFactory theSqlBuilderFactory, 241 HibernatePropertiesProvider theDialectProvider, 242 ISearchParamRegistry theSearchParamRegistry, 243 PartitionSettings thePartitionSettings, 244 IInterceptorBroadcaster theInterceptorBroadcaster, 245 IResourceTagDao theResourceTagDao, 246 DaoRegistry theDaoRegistry, 247 FhirContext theContext, 248 IIdHelperService theIdHelperService, 249 Class<? extends IBaseResource> theResourceType) { 250 myResourceName = theResourceName; 251 myResourceType = theResourceType; 252 myStorageSettings = theStorageSettings; 253 254 myEntityManagerFactory = theEntityManagerFactory; 255 mySqlBuilderFactory = theSqlBuilderFactory; 256 myDialectProvider = theDialectProvider; 257 mySearchParamRegistry = theSearchParamRegistry; 258 myPartitionSettings = thePartitionSettings; 259 myInterceptorBroadcaster = theInterceptorBroadcaster; 260 myResourceTagDao = theResourceTagDao; 261 myDaoRegistry = theDaoRegistry; 262 myContext = theContext; 263 myIdHelperService = theIdHelperService; 264 } 265 266 @VisibleForTesting 267 void setResourceName(String theName) { 268 myResourceName = theName; 269 } 270 271 @Override 272 public void setMaxResultsToFetch(Integer theMaxResultsToFetch) { 273 myMaxResultsToFetch = theMaxResultsToFetch; 274 } 275 276 private void searchForIdsWithAndOr( 277 SearchQueryBuilder theSearchSqlBuilder, 278 QueryStack theQueryStack, 279 @Nonnull SearchParameterMap theParams, 280 RequestDetails theRequest) { 281 myParams = theParams; 282 283 // Remove any empty parameters 284 theParams.clean(); 285 286 // For DSTU3, pull out near-distance first so when it comes time to evaluate near, we already know the distance 287 if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { 288 Dstu3DistanceHelper.setNearDistance(myResourceType, theParams); 289 } 290 291 // Attempt to lookup via composite unique key. 292 if (isCompositeUniqueSpCandidate()) { 293 attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest); 294 } 295 296 // Handle _id and _tag last, since they can typically be tacked onto a different parameter 297 List<String> paramNames = myParams.keySet().stream() 298 .filter(t -> !t.equals(IAnyResource.SP_RES_ID)) 299 .filter(t -> !t.equals(Constants.PARAM_TAG)) 300 .collect(Collectors.toList()); 301 if (myParams.containsKey(IAnyResource.SP_RES_ID)) { 302 paramNames.add(IAnyResource.SP_RES_ID); 303 } 304 if (myParams.containsKey(Constants.PARAM_TAG)) { 305 paramNames.add(Constants.PARAM_TAG); 306 } 307 308 // Handle each parameter 309 for (String nextParamName : paramNames) { 310 if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) { 311 // Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by 312 // Elasticsearch 313 continue; 314 } 315 List<List<IQueryParameterType>> andOrParams = myParams.get(nextParamName); 316 Condition predicate = theQueryStack.searchForIdsWithAndOr(with().setResourceName(myResourceName) 317 .setParamName(nextParamName) 318 .setAndOrParams(andOrParams) 319 .setRequest(theRequest) 320 .setRequestPartitionId(myRequestPartitionId)); 321 if (predicate != null) { 322 theSearchSqlBuilder.addPredicate(predicate); 323 } 324 } 325 } 326 327 /** 328 * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the 329 * parameters all have no modifiers. 330 */ 331 private boolean isCompositeUniqueSpCandidate() { 332 return myStorageSettings.isUniqueIndexesEnabled() && myParams.getEverythingMode() == null; 333 } 334 335 @SuppressWarnings("ConstantConditions") 336 @Override 337 public Long createCountQuery( 338 SearchParameterMap theParams, 339 String theSearchUuid, 340 RequestDetails theRequest, 341 @Nonnull RequestPartitionId theRequestPartitionId) { 342 343 assert theRequestPartitionId != null; 344 assert TransactionSynchronizationManager.isActualTransactionActive(); 345 346 init(theParams, theSearchUuid, theRequestPartitionId); 347 348 if (checkUseHibernateSearch()) { 349 return myFulltextSearchSvc.count(myResourceName, theParams.clone()); 350 } 351 352 List<ISearchQueryExecutor> queries = createQuery(theParams.clone(), null, null, null, true, theRequest, null); 353 if (queries.isEmpty()) { 354 return 0L; 355 } else { 356 JpaPid jpaPid = queries.get(0).next(); 357 return jpaPid.getId(); 358 } 359 } 360 361 /** 362 * @param thePidSet May be null 363 */ 364 @Override 365 public void setPreviouslyAddedResourcePids(@Nonnull List<JpaPid> thePidSet) { 366 myPidSet = new HashSet<>(thePidSet); 367 } 368 369 @SuppressWarnings("ConstantConditions") 370 @Override 371 public IResultIterator<JpaPid> createQuery( 372 SearchParameterMap theParams, 373 SearchRuntimeDetails theSearchRuntimeDetails, 374 RequestDetails theRequest, 375 @Nonnull RequestPartitionId theRequestPartitionId) { 376 assert theRequestPartitionId != null; 377 assert TransactionSynchronizationManager.isActualTransactionActive(); 378 379 init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId); 380 381 if (myPidSet == null) { 382 myPidSet = new HashSet<>(); 383 } 384 385 return new QueryIterator(theSearchRuntimeDetails, theRequest); 386 } 387 388 private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) { 389 myCriteriaBuilder = myEntityManager.getCriteriaBuilder(); 390 // we mutate the params. Make a private copy. 391 myParams = theParams.clone(); 392 mySearchUuid = theSearchUuid; 393 myRequestPartitionId = theRequestPartitionId; 394 } 395 396 private List<ISearchQueryExecutor> createQuery( 397 SearchParameterMap theParams, 398 SortSpec sort, 399 Integer theOffset, 400 Integer theMaximumResults, 401 boolean theCountOnlyFlag, 402 RequestDetails theRequest, 403 SearchRuntimeDetails theSearchRuntimeDetails) { 404 405 ArrayList<ISearchQueryExecutor> queries = new ArrayList<>(); 406 407 if (checkUseHibernateSearch()) { 408 // we're going to run at least part of the search against the Fulltext service. 409 410 // Ugh - we have two different return types for now 411 ISearchQueryExecutor fulltextExecutor = null; 412 List<JpaPid> fulltextMatchIds = null; 413 int resultCount = 0; 414 if (myParams.isLastN()) { 415 fulltextMatchIds = executeLastNAgainstIndex(theMaximumResults); 416 resultCount = fulltextMatchIds.size(); 417 } else if (myParams.getEverythingMode() != null) { 418 fulltextMatchIds = queryHibernateSearchForEverythingPids(theRequest); 419 resultCount = fulltextMatchIds.size(); 420 } else { 421 // todo performance MB - some queries must intersect with JPA (e.g. they have a chain, or we haven't 422 // enabled SP indexing). 423 // and some queries don't need JPA. We only need the scroll when we need to intersect with JPA. 424 // It would be faster to have a non-scrolled search in this case, since creating the scroll requires 425 // extra work in Elastic. 426 // if (eligibleToSkipJPAQuery) fulltextExecutor = myFulltextSearchSvc.searchNotScrolled( ... 427 428 // we might need to intersect with JPA. So we might need to traverse ALL results from lucene, not just 429 // a page. 430 fulltextExecutor = myFulltextSearchSvc.searchScrolled(myResourceName, myParams, theRequest); 431 } 432 433 if (fulltextExecutor == null) { 434 fulltextExecutor = 435 SearchQueryExecutors.from(fulltextMatchIds != null ? fulltextMatchIds : new ArrayList<>()); 436 } 437 438 if (theSearchRuntimeDetails != null) { 439 theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount); 440 IInterceptorBroadcaster compositeBroadcaster = 441 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 442 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE)) { 443 HookParams params = new HookParams() 444 .add(RequestDetails.class, theRequest) 445 .addIfMatchesType(ServletRequestDetails.class, theRequest) 446 .add(SearchRuntimeDetails.class, theSearchRuntimeDetails); 447 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, params); 448 } 449 } 450 451 // can we skip the database entirely and return the pid list from here? 452 boolean canSkipDatabase = 453 // if we processed an AND clause, and it returned nothing, then nothing can match. 454 !fulltextExecutor.hasNext() 455 || 456 // Our hibernate search query doesn't respect partitions yet 457 (!myPartitionSettings.isPartitioningEnabled() 458 && 459 // were there AND terms left? Then we still need the db. 460 theParams.isEmpty() 461 && 462 // not every param is a param. :-( 463 theParams.getNearDistanceParam() == null 464 && 465 // todo MB don't we support _lastUpdated and _offset now? 466 theParams.getLastUpdated() == null 467 && theParams.getEverythingMode() == null 468 && theParams.getOffset() == null); 469 470 if (canSkipDatabase) { 471 ourLog.trace("Query finished after HSearch. Skip db query phase"); 472 if (theMaximumResults != null) { 473 fulltextExecutor = SearchQueryExecutors.limited(fulltextExecutor, theMaximumResults); 474 } 475 queries.add(fulltextExecutor); 476 } else { 477 ourLog.trace("Query needs db after HSearch. Chunking."); 478 // Finish the query in the database for the rest of the search parameters, sorting, partitioning, etc. 479 // We break the pids into chunks that fit in the 1k limit for jdbc bind params. 480 new QueryChunker<JpaPid>() 481 .chunk( 482 fulltextExecutor, 483 SearchBuilder.getMaximumPageSize(), 484 // for each list of (SearchBuilder.getMaximumPageSize()) 485 // we create a chunked query and add it to 'queries' 486 t -> doCreateChunkedQueries( 487 theParams, t, theOffset, sort, theCountOnlyFlag, theRequest, queries)); 488 } 489 } else { 490 // do everything in the database. 491 createChunkedQuery( 492 theParams, sort, theOffset, theMaximumResults, theCountOnlyFlag, theRequest, null, queries); 493 } 494 495 return queries; 496 } 497 498 /** 499 * Check to see if query should use Hibernate Search, and error if the query can't continue. 500 * 501 * @return true if the query should first be processed by Hibernate Search 502 * @throws InvalidRequestException if fulltext search is not enabled but the query requires it - _content or _text 503 */ 504 private boolean checkUseHibernateSearch() { 505 boolean fulltextEnabled = (myFulltextSearchSvc != null) && !myFulltextSearchSvc.isDisabled(); 506 507 if (!fulltextEnabled) { 508 failIfUsed(Constants.PARAM_TEXT); 509 failIfUsed(Constants.PARAM_CONTENT); 510 } else { 511 for (SortSpec sortSpec : myParams.getAllChainsInOrder()) { 512 final String paramName = sortSpec.getParamName(); 513 if (paramName.contains(".")) { 514 failIfUsedWithChainedSort(Constants.PARAM_TEXT); 515 failIfUsedWithChainedSort(Constants.PARAM_CONTENT); 516 } 517 } 518 } 519 520 // someday we'll want a query planner to figure out if we _should_ or _must_ use the ft index, not just if we 521 // can. 522 return fulltextEnabled 523 && myParams != null 524 && myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE 525 && myFulltextSearchSvc.canUseHibernateSearch(myResourceName, myParams) 526 && myFulltextSearchSvc.supportsAllSortTerms(myResourceName, myParams); 527 } 528 529 private void failIfUsed(String theParamName) { 530 if (myParams.containsKey(theParamName)) { 531 throw new InvalidRequestException(Msg.code(1192) 532 + "Fulltext search is not enabled on this service, can not process parameter: " + theParamName); 533 } 534 } 535 536 private void failIfUsedWithChainedSort(String theParamName) { 537 if (myParams.containsKey(theParamName)) { 538 throw new InvalidRequestException(Msg.code(2524) 539 + "Fulltext search combined with chained sorts are not supported, can not process parameter: " 540 + theParamName); 541 } 542 } 543 544 private List<JpaPid> executeLastNAgainstIndex(Integer theMaximumResults) { 545 // Can we use our hibernate search generated index on resource to support lastN?: 546 if (myStorageSettings.isAdvancedHSearchIndexing()) { 547 if (myFulltextSearchSvc == null) { 548 throw new InvalidRequestException(Msg.code(2027) 549 + "LastN operation is not enabled on this service, can not process this request"); 550 } 551 List<IResourcePersistentId> persistentIds = myFulltextSearchSvc.lastN(myParams, theMaximumResults); 552 return persistentIds.stream().map(t -> (JpaPid) t).collect(Collectors.toList()); 553 } else { 554 throw new InvalidRequestException( 555 Msg.code(2033) + "LastN operation is not enabled on this service, can not process this request"); 556 } 557 } 558 559 private List<JpaPid> queryHibernateSearchForEverythingPids(RequestDetails theRequestDetails) { 560 JpaPid pid = null; 561 if (myParams.get(IAnyResource.SP_RES_ID) != null) { 562 String idParamValue; 563 IQueryParameterType idParam = 564 myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); 565 if (idParam instanceof TokenParam) { 566 TokenParam idParm = (TokenParam) idParam; 567 idParamValue = idParm.getValue(); 568 } else { 569 StringParam idParm = (StringParam) idParam; 570 idParamValue = idParm.getValue(); 571 } 572 573 pid = myIdHelperService 574 .resolveResourceIdentity( 575 myRequestPartitionId, 576 myResourceName, 577 idParamValue, 578 ResolveIdentityMode.includeDeleted().cacheOk()) 579 .getPersistentId(); 580 } 581 return myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails); 582 } 583 584 private void doCreateChunkedQueries( 585 SearchParameterMap theParams, 586 List<JpaPid> thePids, 587 Integer theOffset, 588 SortSpec sort, 589 boolean theCount, 590 RequestDetails theRequest, 591 ArrayList<ISearchQueryExecutor> theQueries) { 592 593 if (thePids.size() < getMaximumPageSize()) { 594 thePids = normalizeIdListForInClause(thePids); 595 } 596 createChunkedQuery(theParams, sort, theOffset, thePids.size(), theCount, theRequest, thePids, theQueries); 597 } 598 599 /** 600 * Combs through the params for any _id parameters and extracts the PIDs for them 601 */ 602 private void extractTargetPidsFromIdParams(Set<JpaPid> theTargetPids) { 603 // get all the IQueryParameterType objects 604 // for _id -> these should all be StringParam values 605 HashSet<IIdType> ids = new HashSet<>(); 606 List<List<IQueryParameterType>> params = myParams.get(IAnyResource.SP_RES_ID); 607 for (List<IQueryParameterType> paramList : params) { 608 for (IQueryParameterType param : paramList) { 609 String id; 610 if (param instanceof StringParam) { 611 // we expect all _id values to be StringParams 612 id = ((StringParam) param).getValue(); 613 } else if (param instanceof TokenParam) { 614 id = ((TokenParam) param).getValue(); 615 } else { 616 // we do not expect the _id parameter to be a non-string value 617 throw new IllegalArgumentException( 618 Msg.code(1193) + "_id parameter must be a StringParam or TokenParam"); 619 } 620 621 IIdType idType = myContext.getVersion().newIdType(); 622 if (id.contains("/")) { 623 idType.setValue(id); 624 } else { 625 idType.setValue(myResourceName + "/" + id); 626 } 627 ids.add(idType); 628 } 629 } 630 631 // fetch our target Pids 632 // this will throw if an id is not found 633 Map<IIdType, IResourceLookup<JpaPid>> idToIdentity = myIdHelperService.resolveResourceIdentities( 634 myRequestPartitionId, 635 new ArrayList<>(ids), 636 ResolveIdentityMode.failOnDeleted().noCacheUnlessDeletesDisabled()); 637 638 // add the pids to targetPids 639 for (IResourceLookup<JpaPid> pid : idToIdentity.values()) { 640 theTargetPids.add(pid.getPersistentId()); 641 } 642 } 643 644 private void createChunkedQuery( 645 SearchParameterMap theParams, 646 SortSpec sort, 647 Integer theOffset, 648 Integer theMaximumResults, 649 boolean theCountOnlyFlag, 650 RequestDetails theRequest, 651 List<JpaPid> thePidList, 652 List<ISearchQueryExecutor> theSearchQueryExecutors) { 653 if (myParams.getEverythingMode() != null) { 654 createChunkedQueryForEverythingSearch( 655 theRequest, 656 theParams, 657 theOffset, 658 theMaximumResults, 659 theCountOnlyFlag, 660 thePidList, 661 theSearchQueryExecutors); 662 } else { 663 createChunkedQueryNormalSearch( 664 theParams, sort, theOffset, theCountOnlyFlag, theRequest, thePidList, theSearchQueryExecutors); 665 } 666 } 667 668 private void createChunkedQueryNormalSearch( 669 SearchParameterMap theParams, 670 SortSpec sort, 671 Integer theOffset, 672 boolean theCountOnlyFlag, 673 RequestDetails theRequest, 674 List<JpaPid> thePidList, 675 List<ISearchQueryExecutor> theSearchQueryExecutors) { 676 SearchQueryBuilder sqlBuilder = new SearchQueryBuilder( 677 myContext, 678 myStorageSettings, 679 myPartitionSettings, 680 myRequestPartitionId, 681 myResourceName, 682 mySqlBuilderFactory, 683 myDialectProvider, 684 theCountOnlyFlag); 685 QueryStack queryStack3 = new QueryStack( 686 theRequest, 687 theParams, 688 myStorageSettings, 689 myContext, 690 sqlBuilder, 691 mySearchParamRegistry, 692 myPartitionSettings); 693 694 if (theParams.keySet().size() > 1 695 || theParams.getSort() != null 696 || theParams.keySet().contains(Constants.PARAM_HAS) 697 || isPotentiallyContainedReferenceParameterExistsAtRoot(theParams)) { 698 List<RuntimeSearchParam> activeComboParams = mySearchParamRegistry.getActiveComboSearchParams( 699 myResourceName, theParams.keySet(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 700 if (activeComboParams.isEmpty()) { 701 sqlBuilder.setNeedResourceTableRoot(true); 702 } 703 } 704 705 /* 706 * If we're doing a filter, always use the resource table as the root - This avoids the possibility of 707 * specific filters with ORs as their root from working around the natural resource type / deletion 708 * status / partition IDs built into queries. 709 */ 710 if (theParams.containsKey(Constants.PARAM_FILTER)) { 711 Condition partitionIdPredicate = sqlBuilder 712 .getOrCreateResourceTablePredicateBuilder() 713 .createPartitionIdPredicate(myRequestPartitionId); 714 if (partitionIdPredicate != null) { 715 sqlBuilder.addPredicate(partitionIdPredicate); 716 } 717 } 718 719 // Normal search 720 searchForIdsWithAndOr(sqlBuilder, queryStack3, myParams, theRequest); 721 722 // If we haven't added any predicates yet, we're doing a search for all resources. Make sure we add the 723 // partition ID predicate in that case. 724 if (!sqlBuilder.haveAtLeastOnePredicate()) { 725 Condition partitionIdPredicate = sqlBuilder 726 .getOrCreateResourceTablePredicateBuilder() 727 .createPartitionIdPredicate(myRequestPartitionId); 728 if (partitionIdPredicate != null) { 729 sqlBuilder.addPredicate(partitionIdPredicate); 730 } 731 } 732 733 // Add PID list predicate for full text search and/or lastn operation 734 addPidListPredicate(thePidList, sqlBuilder); 735 736 // Last updated 737 addLastUpdatePredicate(sqlBuilder); 738 739 /* 740 * Exclude the pids already in the previous iterator. This is an optimization, as opposed 741 * to something needed to guarantee correct results. 742 * 743 * Why do we need it? Suppose for example, a query like: 744 * Observation?category=foo,bar,baz 745 * And suppose you have many resources that have all 3 of these category codes. In this case 746 * the SQL query will probably return the same PIDs multiple times, and if this happens enough 747 * we may exhaust the query results without getting enough distinct results back. When that 748 * happens we re-run the query with a larger limit. Excluding results we already know about 749 * tries to ensure that we get new unique results. 750 * 751 * The challenge with that though is that lots of DBs have an issue with too many 752 * parameters in one query. So we only do this optimization if there aren't too 753 * many results. 754 */ 755 if (myHasNextIteratorQuery) { 756 if (myPidSet.size() + sqlBuilder.countBindVariables() < 900) { 757 sqlBuilder.excludeResourceIdsPredicate(myPidSet); 758 } 759 } 760 761 /* 762 * If offset is present, we want deduplicate the results by using GROUP BY 763 */ 764 if (theOffset != null) { 765 queryStack3.addGrouping(); 766 queryStack3.setUseAggregate(true); 767 } 768 769 /* 770 * Sort 771 * 772 * If we have a sort, we wrap the criteria search (the search that actually 773 * finds the appropriate resources) in an outer search which is then sorted 774 */ 775 if (sort != null) { 776 assert !theCountOnlyFlag; 777 778 createSort(queryStack3, sort, theParams); 779 } 780 781 /* 782 * Now perform the search 783 */ 784 executeSearch(theOffset, theSearchQueryExecutors, sqlBuilder); 785 } 786 787 private void executeSearch( 788 Integer theOffset, List<ISearchQueryExecutor> theSearchQueryExecutors, SearchQueryBuilder sqlBuilder) { 789 GeneratedSql generatedSql = sqlBuilder.generate(theOffset, myMaxResultsToFetch); 790 if (!generatedSql.isMatchNothing()) { 791 SearchQueryExecutor executor = 792 mySqlBuilderFactory.newSearchQueryExecutor(generatedSql, myMaxResultsToFetch); 793 theSearchQueryExecutors.add(executor); 794 } 795 } 796 797 private void createChunkedQueryForEverythingSearch( 798 RequestDetails theRequest, 799 SearchParameterMap theParams, 800 Integer theOffset, 801 Integer theMaximumResults, 802 boolean theCountOnlyFlag, 803 List<JpaPid> thePidList, 804 List<ISearchQueryExecutor> theSearchQueryExecutors) { 805 806 SearchQueryBuilder sqlBuilder = new SearchQueryBuilder( 807 myContext, 808 myStorageSettings, 809 myPartitionSettings, 810 myRequestPartitionId, 811 null, 812 mySqlBuilderFactory, 813 myDialectProvider, 814 theCountOnlyFlag); 815 816 QueryStack queryStack3 = new QueryStack( 817 theRequest, 818 theParams, 819 myStorageSettings, 820 myContext, 821 sqlBuilder, 822 mySearchParamRegistry, 823 myPartitionSettings); 824 825 JdbcTemplate jdbcTemplate = initializeJdbcTemplate(theMaximumResults); 826 827 Set<JpaPid> targetPids = new HashSet<>(); 828 if (myParams.get(IAnyResource.SP_RES_ID) != null) { 829 830 extractTargetPidsFromIdParams(targetPids); 831 832 // add the target pids to our executors as the first 833 // results iterator to go through 834 theSearchQueryExecutors.add(new ResolvedSearchQueryExecutor(new ArrayList<>(targetPids))); 835 } else { 836 // For Everything queries, we make the query root by the ResourceLink table, since this query 837 // is basically a reverse-include search. For type/Everything (as opposed to instance/Everything) 838 // the one problem with this approach is that it doesn't catch Patients that have absolutely 839 // nothing linked to them. So we do one additional query to make sure we catch those too. 840 SearchQueryBuilder fetchPidsSqlBuilder = new SearchQueryBuilder( 841 myContext, 842 myStorageSettings, 843 myPartitionSettings, 844 myRequestPartitionId, 845 myResourceName, 846 mySqlBuilderFactory, 847 myDialectProvider, 848 theCountOnlyFlag); 849 GeneratedSql allTargetsSql = fetchPidsSqlBuilder.generate(theOffset, myMaxResultsToFetch); 850 String sql = allTargetsSql.getSql(); 851 Object[] args = allTargetsSql.getBindVariables().toArray(new Object[0]); 852 853 List<JpaPid> output = 854 jdbcTemplate.query(sql, args, new JpaPidRowMapper(myPartitionSettings.isPartitioningEnabled())); 855 856 // we add a search executor to fetch unlinked patients first 857 theSearchQueryExecutors.add(new ResolvedSearchQueryExecutor(output)); 858 } 859 860 List<String> typeSourceResources = new ArrayList<>(); 861 if (myParams.get(Constants.PARAM_TYPE) != null) { 862 typeSourceResources.addAll(extractTypeSourceResourcesFromParams()); 863 } 864 865 queryStack3.addPredicateEverythingOperation( 866 myResourceName, typeSourceResources, targetPids.toArray(EMPTY_JPA_PID_ARRAY)); 867 868 // Add PID list predicate for full text search and/or lastn operation 869 addPidListPredicate(thePidList, sqlBuilder); 870 871 /* 872 * If offset is present, we want deduplicate the results by using GROUP BY 873 * ORDER BY is required to make sure we return unique results for each page 874 */ 875 if (theOffset != null) { 876 queryStack3.addGrouping(); 877 queryStack3.addOrdering(); 878 queryStack3.setUseAggregate(true); 879 } 880 881 /* 882 * Now perform the search 883 */ 884 executeSearch(theOffset, theSearchQueryExecutors, sqlBuilder); 885 } 886 887 private void addPidListPredicate(List<JpaPid> thePidList, SearchQueryBuilder theSqlBuilder) { 888 if (thePidList != null && !thePidList.isEmpty()) { 889 theSqlBuilder.addResourceIdsPredicate(thePidList); 890 } 891 } 892 893 private void addLastUpdatePredicate(SearchQueryBuilder theSqlBuilder) { 894 DateRangeParam lu = myParams.getLastUpdated(); 895 if (lu != null && !lu.isEmpty()) { 896 Condition lastUpdatedPredicates = theSqlBuilder.addPredicateLastUpdated(lu); 897 theSqlBuilder.addPredicate(lastUpdatedPredicates); 898 } 899 } 900 901 private JdbcTemplate initializeJdbcTemplate(Integer theMaximumResults) { 902 JdbcTemplate jdbcTemplate = new JdbcTemplate(myEntityManagerFactory.getDataSource()); 903 jdbcTemplate.setFetchSize(myFetchSize); 904 if (theMaximumResults != null) { 905 jdbcTemplate.setMaxRows(theMaximumResults); 906 } 907 return jdbcTemplate; 908 } 909 910 private Collection<String> extractTypeSourceResourcesFromParams() { 911 912 List<List<IQueryParameterType>> listOfList = myParams.get(Constants.PARAM_TYPE); 913 914 // first off, let's flatten the list of list 915 List<IQueryParameterType> iQueryParameterTypesList = 916 listOfList.stream().flatMap(List::stream).collect(Collectors.toList()); 917 918 // then, extract all elements of each CSV into one big list 919 List<String> resourceTypes = iQueryParameterTypesList.stream() 920 .map(param -> ((StringParam) param).getValue()) 921 .map(csvString -> List.of(csvString.split(","))) 922 .flatMap(List::stream) 923 .collect(Collectors.toList()); 924 925 Set<String> knownResourceTypes = myContext.getResourceTypes(); 926 927 // remove leading/trailing whitespaces if any and remove duplicates 928 Set<String> retVal = new HashSet<>(); 929 930 for (String type : resourceTypes) { 931 String trimmed = type.trim(); 932 if (!knownResourceTypes.contains(trimmed)) { 933 throw new ResourceNotFoundException( 934 Msg.code(2197) + "Unknown resource type '" + trimmed + "' in _type parameter."); 935 } 936 retVal.add(trimmed); 937 } 938 939 return retVal; 940 } 941 942 private boolean isPotentiallyContainedReferenceParameterExistsAtRoot(SearchParameterMap theParams) { 943 return myStorageSettings.isIndexOnContainedResources() 944 && theParams.values().stream() 945 .flatMap(Collection::stream) 946 .flatMap(Collection::stream) 947 .anyMatch(ReferenceParam.class::isInstance); 948 } 949 950 private void createSort(QueryStack theQueryStack, SortSpec theSort, SearchParameterMap theParams) { 951 if (theSort == null || isBlank(theSort.getParamName())) { 952 return; 953 } 954 955 boolean ascending = (theSort.getOrder() == null) || (theSort.getOrder() == SortOrderEnum.ASC); 956 957 if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) { 958 959 theQueryStack.addSortOnResourceId(ascending); 960 961 } else if (Constants.PARAM_PID.equals(theSort.getParamName())) { 962 963 theQueryStack.addSortOnResourcePID(ascending); 964 965 } else if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) { 966 967 theQueryStack.addSortOnLastUpdated(ascending); 968 969 } else { 970 RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam( 971 myResourceName, theSort.getParamName(), ISearchParamRegistry.SearchParamLookupContextEnum.SORT); 972 973 /* 974 * If we have a sort like _sort=subject.name and we have an 975 * uplifted refchain for that combination we can do it more efficiently 976 * by using the index associated with the uplifted refchain. In this case, 977 * we need to find the actual target search parameter (corresponding 978 * to "name" in this example) so that we know what datatype it is. 979 */ 980 String paramName = theSort.getParamName(); 981 if (param == null && myStorageSettings.isIndexOnUpliftedRefchains()) { 982 String[] chains = StringUtils.split(paramName, '.'); 983 if (chains.length == 2) { 984 985 // Given: Encounter?_sort=Patient:subject.name 986 String referenceParam = chains[0]; // subject 987 String referenceParamTargetType = null; // Patient 988 String targetParam = chains[1]; // name 989 990 int colonIdx = referenceParam.indexOf(':'); 991 if (colonIdx > -1) { 992 referenceParamTargetType = referenceParam.substring(0, colonIdx); 993 referenceParam = referenceParam.substring(colonIdx + 1); 994 } 995 RuntimeSearchParam outerParam = mySearchParamRegistry.getActiveSearchParam( 996 myResourceName, referenceParam, ISearchParamRegistry.SearchParamLookupContextEnum.SORT); 997 if (outerParam == null) { 998 throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam); 999 } else if (outerParam.hasUpliftRefchain(targetParam)) { 1000 for (String nextTargetType : outerParam.getTargets()) { 1001 if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) { 1002 continue; 1003 } 1004 RuntimeSearchParam innerParam = mySearchParamRegistry.getActiveSearchParam( 1005 nextTargetType, 1006 targetParam, 1007 ISearchParamRegistry.SearchParamLookupContextEnum.SORT); 1008 if (innerParam != null) { 1009 param = innerParam; 1010 break; 1011 } 1012 } 1013 } 1014 } 1015 } 1016 1017 int colonIdx = paramName.indexOf(':'); 1018 String referenceTargetType = null; 1019 if (colonIdx > -1) { 1020 referenceTargetType = paramName.substring(0, colonIdx); 1021 paramName = paramName.substring(colonIdx + 1); 1022 } 1023 1024 int dotIdx = paramName.indexOf('.'); 1025 String chainName = null; 1026 if (param == null && dotIdx > -1) { 1027 chainName = paramName.substring(dotIdx + 1); 1028 paramName = paramName.substring(0, dotIdx); 1029 if (chainName.contains(".")) { 1030 String msg = myContext 1031 .getLocalizer() 1032 .getMessageSanitized( 1033 BaseStorageDao.class, 1034 "invalidSortParameterTooManyChains", 1035 paramName + "." + chainName); 1036 throw new InvalidRequestException(Msg.code(2286) + msg); 1037 } 1038 } 1039 1040 if (param == null) { 1041 param = mySearchParamRegistry.getActiveSearchParam( 1042 myResourceName, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SORT); 1043 } 1044 1045 if (param == null) { 1046 throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName); 1047 } 1048 1049 // param will never be null here (the above line throws if it does) 1050 // this is just to prevent the warning 1051 assert param != null; 1052 if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { 1053 throw new InvalidRequestException( 1054 Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter"); 1055 } 1056 1057 switch (param.getParamType()) { 1058 case STRING: 1059 theQueryStack.addSortOnString(myResourceName, paramName, ascending); 1060 break; 1061 case DATE: 1062 theQueryStack.addSortOnDate(myResourceName, paramName, ascending); 1063 break; 1064 case REFERENCE: 1065 theQueryStack.addSortOnResourceLink( 1066 myResourceName, referenceTargetType, paramName, chainName, ascending, theParams); 1067 break; 1068 case TOKEN: 1069 theQueryStack.addSortOnToken(myResourceName, paramName, ascending); 1070 break; 1071 case NUMBER: 1072 theQueryStack.addSortOnNumber(myResourceName, paramName, ascending); 1073 break; 1074 case URI: 1075 theQueryStack.addSortOnUri(myResourceName, paramName, ascending); 1076 break; 1077 case QUANTITY: 1078 theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending); 1079 break; 1080 case COMPOSITE: 1081 List<RuntimeSearchParam> compositeList = 1082 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param); 1083 if (compositeList == null) { 1084 throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName 1085 + " is not defined by the resource " + myResourceName); 1086 } 1087 if (compositeList.size() != 2) { 1088 throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName 1089 + " must have 2 composite types declared in parameter annotation, found " 1090 + compositeList.size()); 1091 } 1092 RuntimeSearchParam left = compositeList.get(0); 1093 RuntimeSearchParam right = compositeList.get(1); 1094 1095 createCompositeSort(theQueryStack, left.getParamType(), left.getName(), ascending); 1096 createCompositeSort(theQueryStack, right.getParamType(), right.getName(), ascending); 1097 1098 break; 1099 case SPECIAL: 1100 if (LOCATION_POSITION.equals(param.getPath())) { 1101 theQueryStack.addSortOnCoordsNear(paramName, ascending, theParams); 1102 break; 1103 } 1104 throw new InvalidRequestException( 1105 Msg.code(2306) + "This server does not support _sort specifications of type " 1106 + param.getParamType() + " - Can't serve _sort=" + paramName); 1107 1108 case HAS: 1109 default: 1110 throw new InvalidRequestException( 1111 Msg.code(1197) + "This server does not support _sort specifications of type " 1112 + param.getParamType() + " - Can't serve _sort=" + paramName); 1113 } 1114 } 1115 1116 // Recurse 1117 createSort(theQueryStack, theSort.getChain(), theParams); 1118 } 1119 1120 private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) { 1121 Collection<String> validSearchParameterNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 1122 theResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SORT); 1123 String msg = myContext 1124 .getLocalizer() 1125 .getMessageSanitized( 1126 BaseStorageDao.class, 1127 "invalidSortParameter", 1128 theParamName, 1129 theResourceName, 1130 validSearchParameterNames); 1131 throw new InvalidRequestException(Msg.code(1194) + msg); 1132 } 1133 1134 private void createCompositeSort( 1135 QueryStack theQueryStack, 1136 RestSearchParameterTypeEnum theParamType, 1137 String theParamName, 1138 boolean theAscending) { 1139 1140 switch (theParamType) { 1141 case STRING: 1142 theQueryStack.addSortOnString(myResourceName, theParamName, theAscending); 1143 break; 1144 case DATE: 1145 theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending); 1146 break; 1147 case TOKEN: 1148 theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending); 1149 break; 1150 case QUANTITY: 1151 theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending); 1152 break; 1153 case NUMBER: 1154 case REFERENCE: 1155 case COMPOSITE: 1156 case URI: 1157 case HAS: 1158 case SPECIAL: 1159 default: 1160 throw new InvalidRequestException( 1161 Msg.code(1198) + "Don't know how to handle composite parameter with type of " + theParamType 1162 + " on _sort=" + theParamName); 1163 } 1164 } 1165 1166 private void doLoadPids( 1167 Collection<JpaPid> thePids, 1168 Collection<JpaPid> theIncludedPids, 1169 List<IBaseResource> theResourceListToPopulate, 1170 boolean theForHistoryOperation, 1171 Map<Long, Integer> thePosition) { 1172 1173 Map<JpaPid, Long> resourcePidToVersion = null; 1174 for (JpaPid next : thePids) { 1175 if (next.getVersion() != null && myStorageSettings.isRespectVersionsForSearchIncludes()) { 1176 if (resourcePidToVersion == null) { 1177 resourcePidToVersion = new HashMap<>(); 1178 } 1179 resourcePidToVersion.put(next, next.getVersion()); 1180 } 1181 } 1182 1183 List<JpaPid> versionlessPids = new ArrayList<>(thePids); 1184 if (versionlessPids.size() < getMaximumPageSize()) { 1185 versionlessPids = normalizeIdListForInClause(versionlessPids); 1186 } 1187 1188 // Load the resource bodies 1189 List<ResourceHistoryTable> resourceSearchViewList = 1190 myResourceHistoryTableDao.findCurrentVersionsByResourcePidsAndFetchResourceTable( 1191 JpaPid.toLongList(versionlessPids)); 1192 1193 /* 1194 * If we have specific versions to load, replace the history entries with the 1195 * correct ones 1196 * 1197 * TODO: this could definitely be made more efficient, probably by not loading the wrong 1198 * version entity first, and by batching the fetches. But this is a fairly infrequently 1199 * used feature, and loading history entities by PK is a very efficient query so it's 1200 * not the end of the world 1201 */ 1202 if (resourcePidToVersion != null) { 1203 for (int i = 0; i < resourceSearchViewList.size(); i++) { 1204 ResourceHistoryTable next = resourceSearchViewList.get(i); 1205 JpaPid resourceId = next.getPersistentId(); 1206 Long version = resourcePidToVersion.get(resourceId); 1207 resourceId.setVersion(version); 1208 if (version != null && !version.equals(next.getVersion())) { 1209 ResourceHistoryTable replacement = 1210 myResourceHistoryTableDao.findForIdAndVersion(next.getResourceId(), version); 1211 resourceSearchViewList.set(i, replacement); 1212 } 1213 } 1214 } 1215 1216 // -- preload all tags with tag definition if any 1217 Map<JpaPid, Collection<BaseTag>> tagMap = getResourceTagMap(resourceSearchViewList); 1218 1219 for (ResourceHistoryTable next : resourceSearchViewList) { 1220 if (next.getDeleted() != null) { 1221 continue; 1222 } 1223 1224 Class<? extends IBaseResource> resourceType = 1225 myContext.getResourceDefinition(next.getResourceType()).getImplementingClass(); 1226 1227 JpaPid resourceId = next.getPersistentId(); 1228 1229 if (resourcePidToVersion != null) { 1230 Long version = resourcePidToVersion.get(resourceId); 1231 resourceId.setVersion(version); 1232 } 1233 1234 IBaseResource resource = null; 1235 if (next != null) { 1236 resource = myJpaStorageResourceParser.toResource( 1237 resourceType, next, tagMap.get(JpaPid.fromId(next.getResourceId())), theForHistoryOperation); 1238 } 1239 if (resource == null) { 1240 if (next != null) { 1241 ourLog.warn( 1242 "Unable to find resource {}/{}/_history/{} in database", 1243 next.getResourceType(), 1244 next.getIdDt().getIdPart(), 1245 next.getVersion()); 1246 } else { 1247 ourLog.warn("Unable to find resource in database."); 1248 } 1249 continue; 1250 } 1251 1252 Integer index = thePosition.get(resourceId.getId()); 1253 if (index == null) { 1254 ourLog.warn("Got back unexpected resource PID {}", resourceId); 1255 continue; 1256 } 1257 1258 if (theIncludedPids.contains(resourceId)) { 1259 ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.INCLUDE); 1260 } else { 1261 ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.MATCH); 1262 } 1263 1264 theResourceListToPopulate.set(index, resource); 1265 } 1266 } 1267 1268 private Map<JpaPid, Collection<BaseTag>> getResourceTagMap(Collection<ResourceHistoryTable> theHistoryTables) { 1269 1270 switch (myStorageSettings.getTagStorageMode()) { 1271 case VERSIONED: 1272 return getPidToTagMapVersioned(theHistoryTables); 1273 case NON_VERSIONED: 1274 return getPidToTagMapUnversioned(theHistoryTables); 1275 case INLINE: 1276 default: 1277 return Map.of(); 1278 } 1279 } 1280 1281 @Nonnull 1282 private Map<JpaPid, Collection<BaseTag>> getPidToTagMapVersioned( 1283 Collection<ResourceHistoryTable> theHistoryTables) { 1284 List<Long> idList = new ArrayList<>(theHistoryTables.size()); 1285 1286 // -- find all resource has tags 1287 for (ResourceHistoryTable resource : theHistoryTables) { 1288 if (resource.isHasTags()) { 1289 idList.add(resource.getId()); 1290 } 1291 } 1292 1293 Map<JpaPid, Collection<BaseTag>> tagMap = new HashMap<>(); 1294 1295 // -- no tags 1296 if (idList.isEmpty()) { 1297 return tagMap; 1298 } 1299 1300 // -- get all tags for the idList 1301 Collection<ResourceHistoryTag> tagList = myResourceHistoryTagDao.findByVersionIds(idList); 1302 1303 // -- build the map, key = resourceId, value = list of ResourceTag 1304 JpaPid resourceId; 1305 Collection<BaseTag> tagCol; 1306 for (ResourceHistoryTag tag : tagList) { 1307 1308 resourceId = JpaPid.fromId(tag.getResourceId()); 1309 tagCol = tagMap.get(resourceId); 1310 if (tagCol == null) { 1311 tagCol = new ArrayList<>(); 1312 tagCol.add(tag); 1313 tagMap.put(resourceId, tagCol); 1314 } else { 1315 tagCol.add(tag); 1316 } 1317 } 1318 1319 return tagMap; 1320 } 1321 1322 @Nonnull 1323 private Map<JpaPid, Collection<BaseTag>> getPidToTagMapUnversioned( 1324 Collection<ResourceHistoryTable> theHistoryTables) { 1325 List<JpaPid> idList = new ArrayList<>(theHistoryTables.size()); 1326 1327 // -- find all resource has tags 1328 for (ResourceHistoryTable resource : theHistoryTables) { 1329 if (resource.isHasTags()) { 1330 idList.add(JpaPid.fromId(resource.getResourceId())); 1331 } 1332 } 1333 1334 Map<JpaPid, Collection<BaseTag>> tagMap = new HashMap<>(); 1335 1336 // -- no tags 1337 if (idList.isEmpty()) { 1338 return tagMap; 1339 } 1340 1341 // -- get all tags for the idList 1342 Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(JpaPid.toLongList(idList)); 1343 1344 // -- build the map, key = resourceId, value = list of ResourceTag 1345 JpaPid resourceId; 1346 Collection<BaseTag> tagCol; 1347 for (ResourceTag tag : tagList) { 1348 1349 resourceId = JpaPid.fromId(tag.getResourceId()); 1350 tagCol = tagMap.get(resourceId); 1351 if (tagCol == null) { 1352 tagCol = new ArrayList<>(); 1353 tagCol.add(tag); 1354 tagMap.put(resourceId, tagCol); 1355 } else { 1356 tagCol.add(tag); 1357 } 1358 } 1359 1360 return tagMap; 1361 } 1362 1363 @Override 1364 public void loadResourcesByPid( 1365 Collection<JpaPid> thePids, 1366 Collection<JpaPid> theIncludedPids, 1367 List<IBaseResource> theResourceListToPopulate, 1368 boolean theForHistoryOperation, 1369 RequestDetails theDetails) { 1370 if (thePids.isEmpty()) { 1371 ourLog.debug("The include pids are empty"); 1372 } 1373 1374 // Dupes will cause a crash later anyhow, but this is expensive so only do it 1375 // when running asserts 1376 assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids; 1377 1378 Map<Long, Integer> position = new HashMap<>(); 1379 for (JpaPid next : thePids) { 1380 position.put(next.getId(), theResourceListToPopulate.size()); 1381 theResourceListToPopulate.add(null); 1382 } 1383 1384 // Can we fast track this loading by checking elastic search? 1385 if (isLoadingFromElasticSearchSupported(thePids)) { 1386 try { 1387 theResourceListToPopulate.addAll(loadResourcesFromElasticSearch(thePids)); 1388 return; 1389 1390 } catch (ResourceNotFoundInIndexException theE) { 1391 // some resources were not found in index, so we will inform this and resort to JPA search 1392 ourLog.warn( 1393 "Some resources were not found in index. Make sure all resources were indexed. Resorting to database search."); 1394 } 1395 } 1396 1397 // We only chunk because some jdbc drivers can't handle long param lists. 1398 new QueryChunker<JpaPid>() 1399 .chunk( 1400 thePids, 1401 t -> doLoadPids( 1402 t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position)); 1403 } 1404 1405 /** 1406 * Check if we can load the resources from Hibernate Search instead of the database. 1407 * We assume this is faster. 1408 * <p> 1409 * Hibernate Search only stores the current version, and only if enabled. 1410 * 1411 * @param thePids the pids to check for versioned references 1412 * @return can we fetch from Hibernate Search? 1413 */ 1414 private boolean isLoadingFromElasticSearchSupported(Collection<JpaPid> thePids) { 1415 // is storage enabled? 1416 return myStorageSettings.isStoreResourceInHSearchIndex() 1417 && myStorageSettings.isAdvancedHSearchIndexing() 1418 && 1419 // we don't support history 1420 thePids.stream().noneMatch(p -> p.getVersion() != null) 1421 && 1422 // skip the complexity for metadata in dstu2 1423 myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3); 1424 } 1425 1426 private List<IBaseResource> loadResourcesFromElasticSearch(Collection<JpaPid> thePids) { 1427 // Do we use the fulltextsvc via hibernate-search to load resources or be backwards compatible with older ES 1428 // only impl 1429 // to handle lastN? 1430 if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) { 1431 List<Long> pidList = thePids.stream().map(JpaPid::getId).collect(Collectors.toList()); 1432 1433 return myFulltextSearchSvc.getResources(pidList); 1434 } else if (!Objects.isNull(myParams) && myParams.isLastN()) { 1435 // legacy LastN implementation 1436 return myIElasticsearchSvc.getObservationResources(thePids); 1437 } else { 1438 return Collections.emptyList(); 1439 } 1440 } 1441 1442 /** 1443 * THIS SHOULD RETURN HASHSET and not just Set because we add to it later 1444 * so it can't be Collections.emptySet() or some such thing. 1445 * The JpaPid returned will have resource type populated. 1446 */ 1447 @Override 1448 public Set<JpaPid> loadIncludes( 1449 FhirContext theContext, 1450 EntityManager theEntityManager, 1451 Collection<JpaPid> theMatches, 1452 Collection<Include> theIncludes, 1453 boolean theReverseMode, 1454 DateRangeParam theLastUpdated, 1455 String theSearchIdOrDescription, 1456 RequestDetails theRequest, 1457 Integer theMaxCount) { 1458 SearchBuilderLoadIncludesParameters<JpaPid> parameters = new SearchBuilderLoadIncludesParameters<>(); 1459 parameters.setFhirContext(theContext); 1460 parameters.setEntityManager(theEntityManager); 1461 parameters.setMatches(theMatches); 1462 parameters.setIncludeFilters(theIncludes); 1463 parameters.setReverseMode(theReverseMode); 1464 parameters.setLastUpdated(theLastUpdated); 1465 parameters.setSearchIdOrDescription(theSearchIdOrDescription); 1466 parameters.setRequestDetails(theRequest); 1467 parameters.setMaxCount(theMaxCount); 1468 return loadIncludes(parameters); 1469 } 1470 1471 @Override 1472 public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theParameters) { 1473 Collection<JpaPid> matches = theParameters.getMatches(); 1474 Collection<Include> currentIncludes = theParameters.getIncludeFilters(); 1475 boolean reverseMode = theParameters.isReverseMode(); 1476 EntityManager entityManager = theParameters.getEntityManager(); 1477 Integer maxCount = theParameters.getMaxCount(); 1478 FhirContext fhirContext = theParameters.getFhirContext(); 1479 RequestDetails request = theParameters.getRequestDetails(); 1480 String searchIdOrDescription = theParameters.getSearchIdOrDescription(); 1481 List<String> desiredResourceTypes = theParameters.getDesiredResourceTypes(); 1482 boolean hasDesiredResourceTypes = desiredResourceTypes != null && !desiredResourceTypes.isEmpty(); 1483 IInterceptorBroadcaster compositeBroadcaster = 1484 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, request); 1485 1486 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) { 1487 CurrentThreadCaptureQueriesListener.startCapturing(); 1488 } 1489 if (matches.isEmpty()) { 1490 return new HashSet<>(); 1491 } 1492 if (currentIncludes == null || currentIncludes.isEmpty()) { 1493 return new HashSet<>(); 1494 } 1495 String searchPidFieldName = reverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID; 1496 String searchPartitionIdFieldName = 1497 reverseMode ? MY_TARGET_RESOURCE_PARTITION_ID : MY_SOURCE_RESOURCE_PARTITION_ID; 1498 String findPidFieldName = reverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID; 1499 String findPartitionIdFieldName = 1500 reverseMode ? MY_SOURCE_RESOURCE_PARTITION_ID : MY_TARGET_RESOURCE_PARTITION_ID; 1501 String findResourceTypeFieldName = reverseMode ? MY_SOURCE_RESOURCE_TYPE : MY_TARGET_RESOURCE_TYPE; 1502 String findVersionFieldName = null; 1503 if (!reverseMode && myStorageSettings.isRespectVersionsForSearchIncludes()) { 1504 findVersionFieldName = MY_TARGET_RESOURCE_VERSION; 1505 } 1506 1507 List<JpaPid> nextRoundMatches = new ArrayList<>(matches); 1508 HashSet<JpaPid> allAdded = new HashSet<>(); 1509 HashSet<JpaPid> original = new HashSet<>(matches); 1510 ArrayList<Include> includes = new ArrayList<>(currentIncludes); 1511 1512 int roundCounts = 0; 1513 StopWatch w = new StopWatch(); 1514 1515 boolean addedSomeThisRound; 1516 do { 1517 roundCounts++; 1518 1519 HashSet<JpaPid> pidsToInclude = new HashSet<>(); 1520 1521 for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) { 1522 Include nextInclude = iter.next(); 1523 if (!nextInclude.isRecurse()) { 1524 iter.remove(); 1525 } 1526 1527 // Account for _include=* 1528 boolean matchAll = "*".equals(nextInclude.getValue()); 1529 1530 // Account for _include=[resourceType]:* 1531 String wantResourceType = null; 1532 if (!matchAll) { 1533 if ("*".equals(nextInclude.getParamName())) { 1534 wantResourceType = nextInclude.getParamType(); 1535 matchAll = true; 1536 } 1537 } 1538 1539 if (matchAll) { 1540 loadIncludesMatchAll( 1541 findPidFieldName, 1542 findPartitionIdFieldName, 1543 findResourceTypeFieldName, 1544 findVersionFieldName, 1545 searchPidFieldName, 1546 searchPartitionIdFieldName, 1547 wantResourceType, 1548 reverseMode, 1549 hasDesiredResourceTypes, 1550 nextRoundMatches, 1551 entityManager, 1552 maxCount, 1553 desiredResourceTypes, 1554 pidsToInclude, 1555 request); 1556 } else { 1557 loadIncludesMatchSpecific( 1558 nextInclude, 1559 fhirContext, 1560 findPidFieldName, 1561 findPartitionIdFieldName, 1562 findVersionFieldName, 1563 searchPidFieldName, 1564 searchPartitionIdFieldName, 1565 reverseMode, 1566 nextRoundMatches, 1567 entityManager, 1568 maxCount, 1569 pidsToInclude, 1570 request); 1571 } 1572 } 1573 1574 nextRoundMatches.clear(); 1575 for (JpaPid next : pidsToInclude) { 1576 if (!original.contains(next) && !allAdded.contains(next)) { 1577 nextRoundMatches.add(next); 1578 } 1579 } 1580 1581 addedSomeThisRound = allAdded.addAll(pidsToInclude); 1582 1583 if (maxCount != null && allAdded.size() >= maxCount) { 1584 break; 1585 } 1586 1587 } while (!includes.isEmpty() && !nextRoundMatches.isEmpty() && addedSomeThisRound); 1588 1589 allAdded.removeAll(original); 1590 1591 ourLog.info( 1592 "Loaded {} {} in {} rounds and {} ms for search {}", 1593 allAdded.size(), 1594 reverseMode ? "_revincludes" : "_includes", 1595 roundCounts, 1596 w.getMillisAndRestart(), 1597 searchIdOrDescription); 1598 1599 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) { 1600 callRawSqlHookWithCurrentThreadQueries(request, compositeBroadcaster); 1601 } 1602 1603 // Interceptor call: STORAGE_PREACCESS_RESOURCES 1604 // This can be used to remove results from the search result details before 1605 // the user has a chance to know that they were in the results 1606 if (!allAdded.isEmpty()) { 1607 1608 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 1609 List<JpaPid> includedPidList = new ArrayList<>(allAdded); 1610 JpaPreResourceAccessDetails accessDetails = 1611 new JpaPreResourceAccessDetails(includedPidList, () -> this); 1612 HookParams params = new HookParams() 1613 .add(IPreResourceAccessDetails.class, accessDetails) 1614 .add(RequestDetails.class, request) 1615 .addIfMatchesType(ServletRequestDetails.class, request); 1616 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 1617 1618 for (int i = includedPidList.size() - 1; i >= 0; i--) { 1619 if (accessDetails.isDontReturnResourceAtIndex(i)) { 1620 JpaPid value = includedPidList.remove(i); 1621 if (value != null) { 1622 allAdded.remove(value); 1623 } 1624 } 1625 } 1626 } 1627 } 1628 1629 return allAdded; 1630 } 1631 1632 private void loadIncludesMatchSpecific( 1633 Include nextInclude, 1634 FhirContext fhirContext, 1635 String findPidFieldName, 1636 String findPartitionFieldName, 1637 String findVersionFieldName, 1638 String searchPidFieldName, 1639 String searchPartitionFieldName, 1640 boolean reverseMode, 1641 List<JpaPid> nextRoundMatches, 1642 EntityManager entityManager, 1643 Integer maxCount, 1644 HashSet<JpaPid> pidsToInclude, 1645 RequestDetails theRequest) { 1646 List<String> paths; 1647 1648 // Start replace 1649 RuntimeSearchParam param; 1650 String resType = nextInclude.getParamType(); 1651 if (isBlank(resType)) { 1652 return; 1653 } 1654 RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType); 1655 if (def == null) { 1656 ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue()); 1657 return; 1658 } 1659 1660 String paramName = nextInclude.getParamName(); 1661 if (isNotBlank(paramName)) { 1662 param = mySearchParamRegistry.getActiveSearchParam( 1663 resType, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 1664 } else { 1665 param = null; 1666 } 1667 if (param == null) { 1668 ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue()); 1669 return; 1670 } 1671 1672 paths = param.getPathsSplitForResourceType(resType); 1673 // end replace 1674 1675 Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param); 1676 1677 for (String nextPath : paths) { 1678 String findPidFieldSqlColumn = 1679 findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id"; 1680 String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS; 1681 if (findVersionFieldName != null) { 1682 fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS; 1683 } 1684 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1685 fieldsToLoad += ", r."; 1686 fieldsToLoad += findPartitionFieldName.equals(MY_SOURCE_RESOURCE_PARTITION_ID) 1687 ? "partition_id" 1688 : "target_res_partition_id"; 1689 fieldsToLoad += " as " + PARTITION_ID_ALIAS; 1690 } 1691 1692 // Query for includes lookup has 2 cases 1693 // Case 1: Where target_resource_id is available in hfj_res_link table for local references 1694 // Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical 1695 // url in target_resource_url 1696 1697 // Case 1: 1698 Map<String, Object> localReferenceQueryParams = new HashMap<>(); 1699 1700 String searchPidFieldSqlColumn = 1701 searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id"; 1702 StringBuilder localReferenceQuery = new StringBuilder(); 1703 localReferenceQuery.append("SELECT ").append(fieldsToLoad); 1704 localReferenceQuery.append(" FROM hfj_res_link r "); 1705 localReferenceQuery.append("WHERE r.src_path = :src_path"); 1706 if (!"target_resource_id".equals(searchPidFieldSqlColumn)) { 1707 localReferenceQuery.append(" AND r.target_resource_id IS NOT NULL"); 1708 } 1709 localReferenceQuery 1710 .append(" AND r.") 1711 .append(searchPidFieldSqlColumn) 1712 .append(" IN (:target_pids) "); 1713 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1714 String partitionFieldToSearch = findPartitionFieldName.equals(MY_SOURCE_RESOURCE_PARTITION_ID) 1715 ? "target_res_partition_id" 1716 : "partition_id"; 1717 localReferenceQuery 1718 .append("AND r.") 1719 .append(partitionFieldToSearch) 1720 .append(" = :search_partition_id "); 1721 } 1722 localReferenceQueryParams.put("src_path", nextPath); 1723 // we loop over target_pids later. 1724 if (targetResourceTypes != null) { 1725 if (targetResourceTypes.size() == 1) { 1726 localReferenceQuery.append("AND r.target_resource_type = :target_resource_type "); 1727 localReferenceQueryParams.put( 1728 "target_resource_type", 1729 targetResourceTypes.iterator().next()); 1730 } else { 1731 localReferenceQuery.append("AND r.target_resource_type in (:target_resource_types) "); 1732 localReferenceQueryParams.put("target_resource_types", targetResourceTypes); 1733 } 1734 } 1735 1736 // Case 2: 1737 Pair<String, Map<String, Object>> canonicalQuery = 1738 buildCanonicalUrlQuery(findVersionFieldName, targetResourceTypes, reverseMode, theRequest); 1739 1740 String sql = localReferenceQuery + "UNION " + canonicalQuery.getLeft(); 1741 1742 Map<String, Object> limitParams = new HashMap<>(); 1743 if (maxCount != null) { 1744 LinkedList<Object> bindVariables = new LinkedList<>(); 1745 sql = SearchQueryBuilder.applyLimitToSql( 1746 myDialectProvider.getDialect(), null, maxCount, sql, null, bindVariables); 1747 1748 // The dialect SQL limiter uses positional params, but we're using 1749 // named params here, so we need to replace the positional params 1750 // with equivalent named ones 1751 StringBuilder sb = new StringBuilder(); 1752 for (int i = 0; i < sql.length(); i++) { 1753 char nextChar = sql.charAt(i); 1754 if (nextChar == '?') { 1755 String nextName = "limit" + i; 1756 sb.append(':').append(nextName); 1757 limitParams.put(nextName, bindVariables.removeFirst()); 1758 } else { 1759 sb.append(nextChar); 1760 } 1761 } 1762 sql = sb.toString(); 1763 } 1764 1765 List<Collection<JpaPid>> partitions = partitionBySizeAndPartitionId(nextRoundMatches, getMaximumPageSize()); 1766 for (Collection<JpaPid> nextPartition : partitions) { 1767 Query q = entityManager.createNativeQuery(sql, Tuple.class); 1768 q.setParameter("target_pids", JpaPid.toLongList(nextPartition)); 1769 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1770 q.setParameter( 1771 "search_partition_id", 1772 nextPartition.iterator().next().getPartitionId()); 1773 } 1774 localReferenceQueryParams.forEach(q::setParameter); 1775 canonicalQuery.getRight().forEach(q::setParameter); 1776 limitParams.forEach(q::setParameter); 1777 1778 @SuppressWarnings("unchecked") 1779 List<Tuple> results = q.getResultList(); 1780 for (Tuple result : results) { 1781 if (result != null) { 1782 Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS))); 1783 Long resourceVersion = null; 1784 if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) { 1785 resourceVersion = 1786 NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS))); 1787 } 1788 Integer partitionId = null; 1789 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1790 partitionId = result.get(PARTITION_ID_ALIAS, Integer.class); 1791 } 1792 1793 JpaPid pid = JpaPid.fromIdAndVersion(resourceId, resourceVersion); 1794 pid.setPartitionId(partitionId); 1795 pidsToInclude.add(pid); 1796 } 1797 } 1798 } 1799 } 1800 } 1801 1802 private void loadIncludesMatchAll( 1803 String findPidFieldName, 1804 String findPartitionFieldName, 1805 String findResourceTypeFieldName, 1806 String findVersionFieldName, 1807 String searchPidFieldName, 1808 String searchPartitionFieldName, 1809 String wantResourceType, 1810 boolean reverseMode, 1811 boolean hasDesiredResourceTypes, 1812 List<JpaPid> nextRoundMatches, 1813 EntityManager entityManager, 1814 Integer maxCount, 1815 List<String> desiredResourceTypes, 1816 HashSet<JpaPid> pidsToInclude, 1817 RequestDetails request) { 1818 StringBuilder sqlBuilder = new StringBuilder(); 1819 sqlBuilder.append("SELECT r.").append(findPidFieldName); 1820 sqlBuilder.append(", r.").append(findResourceTypeFieldName); 1821 sqlBuilder.append(", r.myTargetResourceUrl"); 1822 if (findVersionFieldName != null) { 1823 sqlBuilder.append(", r.").append(findVersionFieldName); 1824 } 1825 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1826 sqlBuilder.append(", r.").append(findPartitionFieldName); 1827 } 1828 sqlBuilder.append(" FROM ResourceLink r WHERE "); 1829 1830 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1831 sqlBuilder.append("r.").append(searchPartitionFieldName); 1832 sqlBuilder.append(" = :target_partition_id AND "); 1833 } 1834 1835 sqlBuilder.append("r.").append(searchPidFieldName); 1836 sqlBuilder.append(" IN (:target_pids)"); 1837 1838 /* 1839 * We need to set the resource type in 2 cases only: 1840 * 1) we are in $everything mode 1841 * (where we only want to fetch specific resource types, regardless of what is 1842 * available to fetch) 1843 * 2) we are doing revincludes 1844 * 1845 * Technically if the request is a qualified star (e.g. _include=Observation:*) we 1846 * should always be checking the source resource type on the resource link. We don't 1847 * actually index that column though by default, so in order to try and be efficient 1848 * we don't actually include it for includes (but we do for revincludes). This is 1849 * because for an include, it doesn't really make sense to include a different 1850 * resource type than the one you are searching on. 1851 */ 1852 if (wantResourceType != null && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) { 1853 // because mySourceResourceType is not part of the HFJ_RES_LINK 1854 // index, this might not be the most optimal performance. 1855 // but it is for an $everything operation (and maybe we should update the index) 1856 sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type"); 1857 } else { 1858 wantResourceType = null; 1859 } 1860 1861 // When calling $everything on a Patient instance, we don't want to recurse into new Patient 1862 // resources 1863 // (e.g. via Provenance, List, or Group) when in an $everything operation 1864 if (myParams != null 1865 && myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) { 1866 sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'"); 1867 sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream() 1868 .collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')"))); 1869 } 1870 if (hasDesiredResourceTypes) { 1871 sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)"); 1872 } 1873 1874 String sql = sqlBuilder.toString(); 1875 List<Collection<JpaPid>> partitions = partitionBySizeAndPartitionId(nextRoundMatches, getMaximumPageSize()); 1876 for (Collection<JpaPid> nextPartition : partitions) { 1877 TypedQuery<?> q = entityManager.createQuery(sql, Object[].class); 1878 q.setParameter("target_pids", JpaPid.toLongList(nextPartition)); 1879 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1880 q.setParameter( 1881 "target_partition_id", nextPartition.iterator().next().getPartitionId()); 1882 } 1883 if (wantResourceType != null) { 1884 q.setParameter("want_resource_type", wantResourceType); 1885 } 1886 if (maxCount != null) { 1887 q.setMaxResults(maxCount); 1888 } 1889 if (hasDesiredResourceTypes) { 1890 q.setParameter("desired_target_resource_types", desiredResourceTypes); 1891 } 1892 List<?> results = q.getResultList(); 1893 Set<String> canonicalUrls = null; 1894 for (Object nextRow : results) { 1895 if (nextRow == null) { 1896 // This can happen if there are outgoing references which are canonical or point to 1897 // other servers 1898 continue; 1899 } 1900 1901 Long version = null; 1902 Long resourceId = (Long) ((Object[]) nextRow)[0]; 1903 String resourceType = (String) ((Object[]) nextRow)[1]; 1904 String resourceCanonicalUrl = (String) ((Object[]) nextRow)[2]; 1905 Integer partitionId = null; 1906 int offset = 0; 1907 if (findVersionFieldName != null) { 1908 version = (Long) ((Object[]) nextRow)[3]; 1909 offset++; 1910 } 1911 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 1912 partitionId = ((Integer) ((Object[]) nextRow)[3 + offset]); 1913 } 1914 1915 if (resourceId != null) { 1916 JpaPid pid = JpaPid.fromIdAndVersionAndResourceType(resourceId, version, resourceType); 1917 pid.setPartitionId(partitionId); 1918 pidsToInclude.add(pid); 1919 } else if (resourceCanonicalUrl != null) { 1920 if (canonicalUrls == null) { 1921 canonicalUrls = new HashSet<>(); 1922 } 1923 canonicalUrls.add(resourceCanonicalUrl); 1924 } 1925 } 1926 1927 if (canonicalUrls != null) { 1928 String message = 1929 "Search with _include=* can be inefficient when references using canonical URLs are detected. Use more specific _include values instead."; 1930 firePerformanceWarning(request, message); 1931 loadCanonicalUrls(request, canonicalUrls, entityManager, pidsToInclude, reverseMode); 1932 } 1933 } 1934 } 1935 1936 private void loadCanonicalUrls( 1937 RequestDetails theRequestDetails, 1938 Set<String> theCanonicalUrls, 1939 EntityManager theEntityManager, 1940 HashSet<JpaPid> thePidsToInclude, 1941 boolean theReverse) { 1942 StringBuilder sqlBuilder; 1943 CanonicalUrlTargets canonicalUrlTargets = 1944 calculateIndexUriIdentityHashesForResourceTypes(theRequestDetails, null, theReverse); 1945 List<List<String>> canonicalUrlPartitions = ListUtils.partition( 1946 List.copyOf(theCanonicalUrls), getMaximumPageSize() - canonicalUrlTargets.myHashIdentityValues.size()); 1947 1948 sqlBuilder = new StringBuilder(); 1949 sqlBuilder.append("SELECT "); 1950 if (myPartitionSettings.isPartitioningEnabled()) { 1951 sqlBuilder.append("i.myPartitionIdValue, "); 1952 } 1953 sqlBuilder.append("i.myResourcePid "); 1954 1955 sqlBuilder.append("FROM ResourceIndexedSearchParamUri i "); 1956 sqlBuilder.append("WHERE i.myHashIdentity IN (:hash_identity) "); 1957 sqlBuilder.append("AND i.myUri IN (:uris)"); 1958 1959 String canonicalResSql = sqlBuilder.toString(); 1960 1961 for (Collection<String> nextCanonicalUrlList : canonicalUrlPartitions) { 1962 TypedQuery<Object[]> canonicalResIdQuery = theEntityManager.createQuery(canonicalResSql, Object[].class); 1963 canonicalResIdQuery.setParameter("hash_identity", canonicalUrlTargets.myHashIdentityValues); 1964 canonicalResIdQuery.setParameter("uris", nextCanonicalUrlList); 1965 List<Object[]> results = canonicalResIdQuery.getResultList(); 1966 for (var next : results) { 1967 if (next != null) { 1968 Integer partitionId = null; 1969 Long pid; 1970 if (next.length == 1) { 1971 pid = (Long) next[0]; 1972 } else { 1973 partitionId = (Integer) ((Object[]) next)[0]; 1974 pid = (Long) ((Object[]) next)[1]; 1975 } 1976 if (pid != null) { 1977 thePidsToInclude.add(JpaPid.fromId(pid, partitionId)); 1978 } 1979 } 1980 } 1981 } 1982 } 1983 1984 /** 1985 * Calls Performance Trace Hook 1986 * 1987 * @param request the request deatils 1988 * Sends a raw SQL query to the Pointcut for raw SQL queries. 1989 */ 1990 private void callRawSqlHookWithCurrentThreadQueries( 1991 RequestDetails request, IInterceptorBroadcaster theCompositeBroadcaster) { 1992 SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing(); 1993 HookParams params = new HookParams() 1994 .add(RequestDetails.class, request) 1995 .addIfMatchesType(ServletRequestDetails.class, request) 1996 .add(SqlQueryList.class, capturedQueries); 1997 theCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, params); 1998 } 1999 2000 @Nullable 2001 private static Set<String> computeTargetResourceTypes(Include nextInclude, RuntimeSearchParam param) { 2002 String targetResourceType = defaultString(nextInclude.getParamTargetType(), null); 2003 boolean haveTargetTypesDefinedByParam = param.hasTargets(); 2004 Set<String> targetResourceTypes; 2005 if (targetResourceType != null) { 2006 targetResourceTypes = Set.of(targetResourceType); 2007 } else if (haveTargetTypesDefinedByParam) { 2008 targetResourceTypes = param.getTargets(); 2009 } else { 2010 // all types! 2011 targetResourceTypes = null; 2012 } 2013 return targetResourceTypes; 2014 } 2015 2016 @Nonnull 2017 private Pair<String, Map<String, Object>> buildCanonicalUrlQuery( 2018 String theVersionFieldName, 2019 Set<String> theTargetResourceTypes, 2020 boolean theReverse, 2021 RequestDetails theRequest) { 2022 String fieldsToLoadFromSpidxUriTable = theReverse ? "r.src_resource_id" : "rUri.res_id"; 2023 if (theVersionFieldName != null) { 2024 // canonical-uri references aren't versioned, but we need to match the column count for the UNION 2025 fieldsToLoadFromSpidxUriTable += ", NULL"; 2026 } 2027 2028 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 2029 if (theReverse) { 2030 fieldsToLoadFromSpidxUriTable += ", r.partition_id as " + PARTITION_ID_ALIAS; 2031 } else { 2032 fieldsToLoadFromSpidxUriTable += ", rUri.partition_id as " + PARTITION_ID_ALIAS; 2033 } 2034 } 2035 2036 // The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url. 2037 // But sp_name isn't indexed, so we use hash_identity instead. 2038 CanonicalUrlTargets canonicalUrlTargets = 2039 calculateIndexUriIdentityHashesForResourceTypes(theRequest, theTargetResourceTypes, theReverse); 2040 2041 Map<String, Object> canonicalUriQueryParams = new HashMap<>(); 2042 StringBuilder canonicalUrlQuery = new StringBuilder(); 2043 canonicalUrlQuery 2044 .append("SELECT ") 2045 .append(fieldsToLoadFromSpidxUriTable) 2046 .append(' '); 2047 canonicalUrlQuery.append("FROM hfj_res_link r "); 2048 2049 // join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2 2050 canonicalUrlQuery.append("JOIN hfj_spidx_uri rUri ON ("); 2051 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 2052 canonicalUrlQuery.append("rUri.partition_id IN (:uri_partition_id) AND "); 2053 canonicalUriQueryParams.put("uri_partition_id", canonicalUrlTargets.myPartitionIds); 2054 } 2055 if (canonicalUrlTargets.myHashIdentityValues.size() == 1) { 2056 canonicalUrlQuery.append("rUri.hash_identity = :uri_identity_hash"); 2057 canonicalUriQueryParams.put( 2058 "uri_identity_hash", 2059 canonicalUrlTargets.myHashIdentityValues.iterator().next()); 2060 } else { 2061 canonicalUrlQuery.append("rUri.hash_identity in (:uri_identity_hashes)"); 2062 canonicalUriQueryParams.put("uri_identity_hashes", canonicalUrlTargets.myHashIdentityValues); 2063 } 2064 canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri"); 2065 canonicalUrlQuery.append(")"); 2066 2067 canonicalUrlQuery.append(" WHERE r.src_path = :src_path AND"); 2068 canonicalUrlQuery.append(" r.target_resource_id IS NULL"); 2069 canonicalUrlQuery.append(" AND"); 2070 if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) { 2071 if (theReverse) { 2072 canonicalUrlQuery.append(" rUri.partition_id"); 2073 } else { 2074 canonicalUrlQuery.append(" r.partition_id"); 2075 } 2076 canonicalUrlQuery.append(" = :search_partition_id"); 2077 canonicalUrlQuery.append(" AND"); 2078 } 2079 if (theReverse) { 2080 canonicalUrlQuery.append(" rUri.res_id"); 2081 } else { 2082 canonicalUrlQuery.append(" r.src_resource_id"); 2083 } 2084 canonicalUrlQuery.append(" IN (:target_pids)"); 2085 2086 return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams); 2087 } 2088 2089 @Nonnull 2090 CanonicalUrlTargets calculateIndexUriIdentityHashesForResourceTypes( 2091 RequestDetails theRequestDetails, Set<String> theTargetResourceTypes, boolean theReverse) { 2092 Set<String> targetResourceTypes = theTargetResourceTypes; 2093 if (targetResourceTypes == null) { 2094 /* 2095 * If we don't have a list of valid target types, we need to figure out a list of all 2096 * possible target types in order to perform the search of the URI index table. This is 2097 * because the hash_identity column encodes the resource type, so we'll need a hash 2098 * value for each possible target type. 2099 */ 2100 targetResourceTypes = new HashSet<>(); 2101 Set<String> possibleTypes = myDaoRegistry.getRegisteredDaoTypes(); 2102 if (theReverse) { 2103 // For reverse includes, it is really hard to figure out what types 2104 // are actually potentially pointing to the type we're searching for 2105 // in this context, so let's just assume it could be anything. 2106 targetResourceTypes = possibleTypes; 2107 } else { 2108 for (var next : mySearchParamRegistry 2109 .getActiveSearchParams(myResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 2110 .values() 2111 .stream() 2112 .filter(t -> t.getParamType().equals(RestSearchParameterTypeEnum.REFERENCE)) 2113 .collect(Collectors.toList())) { 2114 2115 // If the reference points to a Reference (ie not a canonical or CanonicalReference) 2116 // then it doesn't matter here anyhow. The logic here only works for elements at the 2117 // root level of the document (e.g. QuestionnaireResponse.subject or 2118 // QuestionnaireResponse.subject.where(...)) but this is just an optimization 2119 // anyhow. 2120 if (next.getPath().startsWith(myResourceName + ".")) { 2121 String elementName = 2122 next.getPath().substring(next.getPath().indexOf('.') + 1); 2123 int secondDotIndex = elementName.indexOf('.'); 2124 if (secondDotIndex != -1) { 2125 elementName = elementName.substring(0, secondDotIndex); 2126 } 2127 BaseRuntimeChildDefinition child = 2128 myContext.getResourceDefinition(myResourceName).getChildByName(elementName); 2129 if (child != null) { 2130 BaseRuntimeElementDefinition<?> childDef = child.getChildByName(elementName); 2131 if (childDef != null) { 2132 if (childDef.getName().equals("Reference")) { 2133 continue; 2134 } 2135 } 2136 } 2137 } 2138 2139 if (!next.getTargets().isEmpty()) { 2140 // For each reference parameter on the resource type we're searching for, 2141 // add all the potential target types to the list of possible target 2142 // resource types we can look up. 2143 for (var nextTarget : next.getTargets()) { 2144 if (possibleTypes.contains(nextTarget)) { 2145 targetResourceTypes.add(nextTarget); 2146 } 2147 } 2148 } else { 2149 // If we have any references that don't define any target types, then 2150 // we need to assume that all enabled resource types are possible target 2151 // types 2152 targetResourceTypes.addAll(possibleTypes); 2153 break; 2154 } 2155 } 2156 } 2157 } 2158 assert !targetResourceTypes.isEmpty(); 2159 2160 Set<Long> hashIdentityValues = new HashSet<>(); 2161 Set<Integer> partitionIds = new HashSet<>(); 2162 for (String type : targetResourceTypes) { 2163 2164 RequestPartitionId readPartition; 2165 if (myPartitionSettings.isPartitioningEnabled()) { 2166 readPartition = 2167 myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, type); 2168 } else { 2169 readPartition = RequestPartitionId.defaultPartition(); 2170 } 2171 if (readPartition.hasPartitionIds()) { 2172 partitionIds.addAll(readPartition.getPartitionIds()); 2173 } 2174 2175 Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity( 2176 myPartitionSettings, readPartition, type, "url"); 2177 hashIdentityValues.add(hashIdentity); 2178 } 2179 2180 return new CanonicalUrlTargets(hashIdentityValues, partitionIds); 2181 } 2182 2183 static class CanonicalUrlTargets { 2184 2185 @Nonnull 2186 final Set<Long> myHashIdentityValues; 2187 2188 @Nonnull 2189 final Set<Integer> myPartitionIds; 2190 2191 public CanonicalUrlTargets(@Nonnull Set<Long> theHashIdentityValues, @Nonnull Set<Integer> thePartitionIds) { 2192 myHashIdentityValues = theHashIdentityValues; 2193 myPartitionIds = thePartitionIds; 2194 } 2195 } 2196 2197 /** 2198 * This method takes in a list of {@link JpaPid}'s and returns a series of sublists containing 2199 * those pids where: 2200 * <ul> 2201 * <li>No single list is most than {@literal theMaxLoad} entries</li> 2202 * <li>Each list only contains JpaPids with the same partition ID</li> 2203 * </ul> 2204 */ 2205 static List<Collection<JpaPid>> partitionBySizeAndPartitionId(List<JpaPid> theNextRoundMatches, int theMaxLoad) { 2206 2207 if (theNextRoundMatches.size() <= theMaxLoad) { 2208 boolean allSamePartition = true; 2209 for (int i = 1; i < theNextRoundMatches.size(); i++) { 2210 if (!Objects.equals( 2211 theNextRoundMatches.get(i - 1).getPartitionId(), 2212 theNextRoundMatches.get(i).getPartitionId())) { 2213 allSamePartition = false; 2214 break; 2215 } 2216 } 2217 if (allSamePartition) { 2218 return Collections.singletonList(theNextRoundMatches); 2219 } 2220 } 2221 2222 // Break into partitioned sublists 2223 ListMultimap<String, JpaPid> lists = 2224 MultimapBuilder.hashKeys().arrayListValues().build(); 2225 for (JpaPid nextRoundMatch : theNextRoundMatches) { 2226 String partitionId = nextRoundMatch.getPartitionId() != null 2227 ? nextRoundMatch.getPartitionId().toString() 2228 : ""; 2229 lists.put(partitionId, nextRoundMatch); 2230 } 2231 2232 List<Collection<JpaPid>> retVal = new ArrayList<>(); 2233 for (String key : lists.keySet()) { 2234 List<List<JpaPid>> nextPartition = Lists.partition(lists.get(key), theMaxLoad); 2235 retVal.addAll(nextPartition); 2236 } 2237 2238 // In unit test mode, we sort the results just for unit test predictability 2239 if (HapiSystemProperties.isUnitTestModeEnabled()) { 2240 retVal = retVal.stream() 2241 .map(t -> t.stream().sorted().collect(Collectors.toList())) 2242 .collect(Collectors.toList()); 2243 } 2244 2245 return retVal; 2246 } 2247 2248 private void attemptComboUniqueSpProcessing( 2249 QueryStack theQueryStack, @Nonnull SearchParameterMap theParams, RequestDetails theRequest) { 2250 RuntimeSearchParam comboParam = null; 2251 List<String> comboParamNames = null; 2252 List<RuntimeSearchParam> exactMatchParams = mySearchParamRegistry.getActiveComboSearchParams( 2253 myResourceName, theParams.keySet(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2254 if (!exactMatchParams.isEmpty()) { 2255 comboParam = exactMatchParams.get(0); 2256 comboParamNames = new ArrayList<>(theParams.keySet()); 2257 } 2258 2259 if (comboParam == null) { 2260 List<RuntimeSearchParam> candidateComboParams = mySearchParamRegistry.getActiveComboSearchParams( 2261 myResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2262 for (RuntimeSearchParam nextCandidate : candidateComboParams) { 2263 List<String> nextCandidateParamNames = 2264 JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream() 2265 .map(RuntimeSearchParam::getName) 2266 .collect(Collectors.toList()); 2267 if (theParams.keySet().containsAll(nextCandidateParamNames)) { 2268 comboParam = nextCandidate; 2269 comboParamNames = nextCandidateParamNames; 2270 break; 2271 } 2272 } 2273 } 2274 2275 if (comboParam != null) { 2276 Collections.sort(comboParamNames); 2277 2278 // Since we're going to remove elements below 2279 theParams.values().forEach(this::ensureSubListsAreWritable); 2280 2281 /* 2282 * Apply search against the combo param index in a loop: 2283 * 2284 * 1. First we check whether the actual parameter values in the 2285 * parameter map are actually usable for searching against the combo 2286 * param index. E.g. no search modifiers, date comparators, etc., 2287 * since these mean you can't use the combo index. 2288 * 2289 * 2. Apply and create the join SQl. We remove parameter values from 2290 * the map as we apply them, so any parameter values remaining in the 2291 * map after each loop haven't yet been factored into the SQL. 2292 * 2293 * The loop allows us to create multiple combo index joins if there 2294 * are multiple AND expressions for the related parameters. 2295 */ 2296 while (validateParamValuesAreValidForComboParam(theRequest, theParams, comboParamNames, comboParam)) { 2297 applyComboSearchParam(theQueryStack, theParams, theRequest, comboParamNames, comboParam); 2298 } 2299 } 2300 } 2301 2302 private void applyComboSearchParam( 2303 QueryStack theQueryStack, 2304 @Nonnull SearchParameterMap theParams, 2305 RequestDetails theRequest, 2306 List<String> theComboParamNames, 2307 RuntimeSearchParam theComboParam) { 2308 2309 List<List<IQueryParameterType>> inputs = new ArrayList<>(); 2310 for (String nextParamName : theComboParamNames) { 2311 List<IQueryParameterType> nextValues = theParams.get(nextParamName).remove(0); 2312 inputs.add(nextValues); 2313 } 2314 2315 List<List<IQueryParameterType>> inputPermutations = Lists.cartesianProduct(inputs); 2316 List<String> indexStrings = new ArrayList<>(CartesianProductUtil.calculateCartesianProductSize(inputs)); 2317 for (List<IQueryParameterType> nextPermutation : inputPermutations) { 2318 2319 StringBuilder searchStringBuilder = new StringBuilder(); 2320 searchStringBuilder.append(myResourceName); 2321 searchStringBuilder.append("?"); 2322 2323 boolean first = true; 2324 for (int paramIndex = 0; paramIndex < theComboParamNames.size(); paramIndex++) { 2325 2326 String nextParamName = theComboParamNames.get(paramIndex); 2327 IQueryParameterType nextOr = nextPermutation.get(paramIndex); 2328 String nextOrValue = nextOr.getValueAsQueryToken(myContext); 2329 2330 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam( 2331 myResourceName, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2332 if (theComboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) { 2333 if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) { 2334 nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue); 2335 } 2336 } 2337 2338 if (first) { 2339 first = false; 2340 } else { 2341 searchStringBuilder.append('&'); 2342 } 2343 2344 nextParamName = UrlUtil.escapeUrlParam(nextParamName); 2345 nextOrValue = UrlUtil.escapeUrlParam(nextOrValue); 2346 2347 searchStringBuilder.append(nextParamName).append('=').append(nextOrValue); 2348 } 2349 2350 String indexString = searchStringBuilder.toString(); 2351 ourLog.debug( 2352 "Checking for {} combo index for query: {}", theComboParam.getComboSearchParamType(), indexString); 2353 2354 indexStrings.add(indexString); 2355 } 2356 2357 // Just to make sure we're stable for tests 2358 indexStrings.sort(Comparator.naturalOrder()); 2359 2360 // Interceptor broadcast: JPA_PERFTRACE_INFO 2361 IInterceptorBroadcaster compositeBroadcaster = 2362 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 2363 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 2364 String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0); 2365 StorageProcessingMessage msg = new StorageProcessingMessage() 2366 .setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: " 2367 + indexStringForLog); 2368 HookParams params = new HookParams() 2369 .add(RequestDetails.class, theRequest) 2370 .addIfMatchesType(ServletRequestDetails.class, theRequest) 2371 .add(StorageProcessingMessage.class, msg); 2372 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 2373 } 2374 2375 switch (requireNonNull(theComboParam.getComboSearchParamType())) { 2376 case UNIQUE: 2377 theQueryStack.addPredicateCompositeUnique(indexStrings, myRequestPartitionId); 2378 break; 2379 case NON_UNIQUE: 2380 theQueryStack.addPredicateCompositeNonUnique(indexStrings, myRequestPartitionId); 2381 break; 2382 } 2383 2384 // Remove any empty parameters remaining after this 2385 theParams.clean(); 2386 } 2387 2388 /** 2389 * Returns {@literal true} if the actual parameter instances in a given query are actually usable for 2390 * searching against a combo param with the given parameter names. This might be {@literal false} if 2391 * parameters have modifiers (e.g. <code>?name:exact=SIMPSON</code>), prefixes 2392 * (e.g. <code>?date=gt2024-02-01</code>), etc. 2393 */ 2394 private boolean validateParamValuesAreValidForComboParam( 2395 RequestDetails theRequest, 2396 @Nonnull SearchParameterMap theParams, 2397 List<String> theComboParamNames, 2398 RuntimeSearchParam theComboParam) { 2399 boolean paramValuesAreValidForCombo = true; 2400 List<List<IQueryParameterType>> paramOrValues = new ArrayList<>(theComboParamNames.size()); 2401 2402 for (String nextParamName : theComboParamNames) { 2403 List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName); 2404 2405 if (nextValues == null || nextValues.isEmpty()) { 2406 paramValuesAreValidForCombo = false; 2407 break; 2408 } 2409 2410 List<IQueryParameterType> nextAndValue = nextValues.get(0); 2411 paramOrValues.add(nextAndValue); 2412 2413 for (IQueryParameterType nextOrValue : nextAndValue) { 2414 if (nextOrValue instanceof DateParam) { 2415 DateParam dateParam = (DateParam) nextOrValue; 2416 if (dateParam.getPrecision() != TemporalPrecisionEnum.DAY) { 2417 String message = "Search with params " + theComboParamNames 2418 + " is not a candidate for combo searching - Date search with non-DAY precision for parameter '" 2419 + nextParamName + "'"; 2420 firePerformanceInfo(theRequest, message); 2421 paramValuesAreValidForCombo = false; 2422 break; 2423 } 2424 } 2425 if (nextOrValue instanceof BaseParamWithPrefix) { 2426 BaseParamWithPrefix<?> paramWithPrefix = (BaseParamWithPrefix<?>) nextOrValue; 2427 if (paramWithPrefix.getPrefix() != null) { 2428 String message = "Search with params " + theComboParamNames 2429 + " is not a candidate for combo searching - Parameter '" + nextParamName 2430 + "' has prefix: '" 2431 + paramWithPrefix.getPrefix().getValue() + "'"; 2432 firePerformanceInfo(theRequest, message); 2433 paramValuesAreValidForCombo = false; 2434 break; 2435 } 2436 } 2437 if (isNotBlank(nextOrValue.getQueryParameterQualifier())) { 2438 String message = "Search with params " + theComboParamNames 2439 + " is not a candidate for combo searching - Parameter '" + nextParamName 2440 + "' has modifier: '" + nextOrValue.getQueryParameterQualifier() + "'"; 2441 firePerformanceInfo(theRequest, message); 2442 paramValuesAreValidForCombo = false; 2443 break; 2444 } 2445 } 2446 2447 // Reference params are only eligible for using a composite index if they 2448 // are qualified 2449 RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam( 2450 myResourceName, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 2451 if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 2452 ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0); 2453 if (isBlank(param.getResourceType())) { 2454 ourLog.debug( 2455 "Search is not a candidate for unique combo searching - Reference with no type specified"); 2456 paramValuesAreValidForCombo = false; 2457 break; 2458 } 2459 } 2460 2461 // Date params are not eligible for using composite unique index 2462 // as index could contain date with different precision (e.g. DAY, SECOND) 2463 if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.DATE 2464 && theComboParam.getComboSearchParamType() == ComboSearchParamType.UNIQUE) { 2465 ourLog.debug( 2466 "Search with params {} is not a candidate for combo searching - " 2467 + "Unique combo search parameter '{}' has DATE type", 2468 theComboParamNames, 2469 nextParamName); 2470 paramValuesAreValidForCombo = false; 2471 break; 2472 } 2473 } 2474 2475 if (CartesianProductUtil.calculateCartesianProductSize(paramOrValues) > 500) { 2476 ourLog.debug( 2477 "Search is not a candidate for unique combo searching - Too many OR values would result in too many permutations"); 2478 paramValuesAreValidForCombo = false; 2479 } 2480 2481 return paramValuesAreValidForCombo; 2482 } 2483 2484 private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) { 2485 for (int i = 0; i < theListOfLists.size(); i++) { 2486 List<T> oldSubList = theListOfLists.get(i); 2487 if (!(oldSubList instanceof ArrayList)) { 2488 List<T> newSubList = new ArrayList<>(oldSubList); 2489 theListOfLists.set(i, newSubList); 2490 } 2491 } 2492 } 2493 2494 @Override 2495 public void setFetchSize(int theFetchSize) { 2496 myFetchSize = theFetchSize; 2497 } 2498 2499 public SearchParameterMap getParams() { 2500 return myParams; 2501 } 2502 2503 public CriteriaBuilder getBuilder() { 2504 return myCriteriaBuilder; 2505 } 2506 2507 public Class<? extends IBaseResource> getResourceType() { 2508 return myResourceType; 2509 } 2510 2511 public String getResourceName() { 2512 return myResourceName; 2513 } 2514 2515 /** 2516 * IncludesIterator, used to recursively fetch resources from the provided list of PIDs 2517 */ 2518 public class IncludesIterator extends BaseIterator<JpaPid> implements Iterator<JpaPid> { 2519 2520 private final RequestDetails myRequest; 2521 private final Set<JpaPid> myCurrentPids; 2522 private Iterator<JpaPid> myCurrentIterator; 2523 private JpaPid myNext; 2524 2525 IncludesIterator(Set<JpaPid> thePidSet, RequestDetails theRequest) { 2526 myCurrentPids = new HashSet<>(thePidSet); 2527 myCurrentIterator = null; 2528 myRequest = theRequest; 2529 } 2530 2531 private void fetchNext() { 2532 while (myNext == null) { 2533 2534 if (myCurrentIterator == null) { 2535 Set<Include> includes = new HashSet<>(); 2536 if (myParams.containsKey(Constants.PARAM_TYPE)) { 2537 for (List<IQueryParameterType> typeList : myParams.get(Constants.PARAM_TYPE)) { 2538 for (IQueryParameterType type : typeList) { 2539 String queryString = ParameterUtil.unescape(type.getValueAsQueryToken(myContext)); 2540 for (String resourceType : queryString.split(",")) { 2541 String rt = resourceType.trim(); 2542 if (isNotBlank(rt)) { 2543 includes.add(new Include(rt + ":*", true)); 2544 } 2545 } 2546 } 2547 } 2548 } 2549 if (includes.isEmpty()) { 2550 includes.add(new Include("*", true)); 2551 } 2552 Set<JpaPid> newPids = loadIncludes( 2553 myContext, 2554 myEntityManager, 2555 myCurrentPids, 2556 includes, 2557 false, 2558 getParams().getLastUpdated(), 2559 mySearchUuid, 2560 myRequest, 2561 null); 2562 myCurrentIterator = newPids.iterator(); 2563 } 2564 2565 if (myCurrentIterator.hasNext()) { 2566 myNext = myCurrentIterator.next(); 2567 } else { 2568 myNext = NO_MORE; 2569 } 2570 } 2571 } 2572 2573 @Override 2574 public boolean hasNext() { 2575 fetchNext(); 2576 return !NO_MORE.equals(myNext); 2577 } 2578 2579 @Override 2580 public JpaPid next() { 2581 fetchNext(); 2582 JpaPid retVal = myNext; 2583 myNext = null; 2584 return retVal; 2585 } 2586 } 2587 2588 /** 2589 * Basic Query iterator, used to fetch the results of a query. 2590 */ 2591 private final class QueryIterator extends BaseIterator<JpaPid> implements IResultIterator<JpaPid> { 2592 2593 private final SearchRuntimeDetails mySearchRuntimeDetails; 2594 private final RequestDetails myRequest; 2595 private final boolean myHaveRawSqlHooks; 2596 private final boolean myHavePerfTraceFoundIdHook; 2597 private final SortSpec mySort; 2598 private final Integer myOffset; 2599 private final IInterceptorBroadcaster myCompositeBroadcaster; 2600 private boolean myFirst = true; 2601 private IncludesIterator myIncludesIterator; 2602 /** 2603 * The next JpaPid value of the next result in this query. 2604 * Will not be null if fetched using getNext() 2605 */ 2606 private JpaPid myNext; 2607 /** 2608 * The current query result iterator running sql and supplying PIDs 2609 * @see #myQueryList 2610 */ 2611 private ISearchQueryExecutor myResultsIterator; 2612 2613 private boolean myFetchIncludesForEverythingOperation; 2614 /** 2615 * The count of resources skipped because they were seen in earlier results 2616 */ 2617 private int mySkipCount = 0; 2618 /** 2619 * The count of resources that are new in this search 2620 * (ie, not cached in previous searches) 2621 */ 2622 private int myNonSkipCount = 0; 2623 2624 /** 2625 * The list of queries to use to find all results. 2626 * Normal JPA queries will normally have a single entry. 2627 * Queries that involve Hibernate Search/Elastisearch may have 2628 * multiple queries because of chunking. 2629 * The $everything operation also jams some extra results in. 2630 */ 2631 private List<ISearchQueryExecutor> myQueryList = new ArrayList<>(); 2632 2633 private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) { 2634 mySearchRuntimeDetails = theSearchRuntimeDetails; 2635 mySort = myParams.getSort(); 2636 myOffset = myParams.getOffset(); 2637 myRequest = theRequest; 2638 myCompositeBroadcaster = 2639 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 2640 2641 // everything requires fetching recursively all related resources 2642 if (myParams.getEverythingMode() != null) { 2643 myFetchIncludesForEverythingOperation = true; 2644 } 2645 2646 myHavePerfTraceFoundIdHook = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID); 2647 myHaveRawSqlHooks = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL); 2648 } 2649 2650 private void fetchNext() { 2651 try { 2652 if (myHaveRawSqlHooks) { 2653 CurrentThreadCaptureQueriesListener.startCapturing(); 2654 } 2655 2656 // If we don't have a query yet, create one 2657 if (myResultsIterator == null) { 2658 if (myMaxResultsToFetch == null) { 2659 myMaxResultsToFetch = calculateMaxResultsToFetch(); 2660 } 2661 2662 /* 2663 * assigns the results iterator 2664 * and populates the myQueryList. 2665 */ 2666 initializeIteratorQuery(myOffset, myMaxResultsToFetch); 2667 } 2668 2669 if (myNext == null) { 2670 // no next means we need a new query (if one is available) 2671 while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) { 2672 /* 2673 * Because we combine our DB searches with Lucene 2674 * sometimes we can have multiple results iterators 2675 * (with only some having data in them to extract). 2676 * 2677 * We'll iterate our results iterators until we 2678 * either run out of results iterators, or we 2679 * have one that actually has data in it. 2680 */ 2681 while (!myResultsIterator.hasNext() && !myQueryList.isEmpty()) { 2682 retrieveNextIteratorQuery(); 2683 } 2684 2685 if (!myResultsIterator.hasNext()) { 2686 // we couldn't find a results iterator; 2687 // we're done here 2688 break; 2689 } 2690 2691 JpaPid nextPid = myResultsIterator.next(); 2692 if (myHavePerfTraceFoundIdHook) { 2693 callPerformanceTracingHook(nextPid); 2694 } 2695 2696 if (nextPid != null) { 2697 if (myPidSet.add(nextPid) && doNotSkipNextPidForEverything()) { 2698 myNext = nextPid; 2699 myNonSkipCount++; 2700 break; 2701 } else { 2702 mySkipCount++; 2703 } 2704 } 2705 2706 if (!myResultsIterator.hasNext()) { 2707 if (myMaxResultsToFetch != null && (mySkipCount + myNonSkipCount == myMaxResultsToFetch)) { 2708 if (mySkipCount > 0 && myNonSkipCount == 0) { 2709 2710 sendProcessingMsgAndFirePerformanceHook(); 2711 2712 myMaxResultsToFetch += 1000; 2713 initializeIteratorQuery(myOffset, myMaxResultsToFetch); 2714 } 2715 } 2716 } 2717 } 2718 } 2719 2720 if (myNext == null) { 2721 // if we got here, it means the current JpaPid has already been processed, 2722 // and we will decide (here) if we need to fetch related resources recursively 2723 if (myFetchIncludesForEverythingOperation) { 2724 myIncludesIterator = new IncludesIterator(myPidSet, myRequest); 2725 myFetchIncludesForEverythingOperation = false; 2726 } 2727 if (myIncludesIterator != null) { 2728 while (myIncludesIterator.hasNext()) { 2729 JpaPid next = myIncludesIterator.next(); 2730 if (next != null && myPidSet.add(next) && doNotSkipNextPidForEverything()) { 2731 myNext = next; 2732 break; 2733 } 2734 } 2735 if (myNext == null) { 2736 myNext = NO_MORE; 2737 } 2738 } else { 2739 myNext = NO_MORE; 2740 } 2741 } 2742 2743 mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size()); 2744 2745 } finally { 2746 // search finished - fire hooks 2747 if (myHaveRawSqlHooks) { 2748 callRawSqlHookWithCurrentThreadQueries(myRequest, myCompositeBroadcaster); 2749 } 2750 } 2751 2752 if (myFirst) { 2753 HookParams params = new HookParams() 2754 .add(RequestDetails.class, myRequest) 2755 .addIfMatchesType(ServletRequestDetails.class, myRequest) 2756 .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); 2757 myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params); 2758 myFirst = false; 2759 } 2760 2761 if (NO_MORE.equals(myNext)) { 2762 HookParams params = new HookParams() 2763 .add(RequestDetails.class, myRequest) 2764 .addIfMatchesType(ServletRequestDetails.class, myRequest) 2765 .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); 2766 myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params); 2767 } 2768 } 2769 2770 private Integer calculateMaxResultsToFetch() { 2771 if (myParams.getLoadSynchronousUpTo() != null) { 2772 return myParams.getLoadSynchronousUpTo(); 2773 } else if (myParams.getOffset() != null && myParams.getCount() != null) { 2774 return myParams.getEverythingMode() != null 2775 ? myParams.getOffset() + myParams.getCount() 2776 : myParams.getCount(); 2777 } else { 2778 return myStorageSettings.getFetchSizeDefaultMaximum(); 2779 } 2780 } 2781 2782 private boolean doNotSkipNextPidForEverything() { 2783 return !(myParams.getEverythingMode() != null && (myOffset != null && myOffset >= myPidSet.size())); 2784 } 2785 2786 private void callPerformanceTracingHook(JpaPid theNextPid) { 2787 HookParams params = new HookParams() 2788 .add(Integer.class, System.identityHashCode(this)) 2789 .add(Object.class, theNextPid); 2790 myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params); 2791 } 2792 2793 private void sendProcessingMsgAndFirePerformanceHook() { 2794 String msg = "Pass completed with no matching results seeking rows " 2795 + myPidSet.size() + "-" + mySkipCount 2796 + ". This indicates an inefficient query! Retrying with new max count of " 2797 + myMaxResultsToFetch; 2798 firePerformanceWarning(myRequest, msg); 2799 } 2800 2801 private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) { 2802 Integer offset = theOffset; 2803 if (myQueryList.isEmpty()) { 2804 // Capture times for Lucene/Elasticsearch queries as well 2805 mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); 2806 2807 // setting offset to 0 to fetch all resource ids to guarantee 2808 // correct output result for everything operation during paging 2809 if (myParams.getEverythingMode() != null) { 2810 offset = 0; 2811 } 2812 myQueryList = createQuery( 2813 myParams, mySort, offset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails); 2814 } 2815 2816 mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); 2817 2818 retrieveNextIteratorQuery(); 2819 2820 mySkipCount = 0; 2821 myNonSkipCount = 0; 2822 } 2823 2824 private void retrieveNextIteratorQuery() { 2825 close(); 2826 if (isNotEmpty(myQueryList)) { 2827 myResultsIterator = myQueryList.remove(0); 2828 myHasNextIteratorQuery = true; 2829 } else { 2830 myResultsIterator = SearchQueryExecutor.emptyExecutor(); 2831 myHasNextIteratorQuery = false; 2832 } 2833 } 2834 2835 @Override 2836 public boolean hasNext() { 2837 if (myNext == null) { 2838 fetchNext(); 2839 } 2840 return !NO_MORE.equals(myNext); 2841 } 2842 2843 @Override 2844 public JpaPid next() { 2845 fetchNext(); 2846 JpaPid retVal = myNext; 2847 myNext = null; 2848 Validate.isTrue(!NO_MORE.equals(retVal), "No more elements"); 2849 return retVal; 2850 } 2851 2852 @Override 2853 public int getSkippedCount() { 2854 return mySkipCount; 2855 } 2856 2857 @Override 2858 public int getNonSkippedCount() { 2859 return myNonSkipCount; 2860 } 2861 2862 @Override 2863 public Collection<JpaPid> getNextResultBatch(long theBatchSize) { 2864 Collection<JpaPid> batch = new ArrayList<>(); 2865 while (this.hasNext() && batch.size() < theBatchSize) { 2866 batch.add(this.next()); 2867 } 2868 return batch; 2869 } 2870 2871 @Override 2872 public void close() { 2873 if (myResultsIterator != null) { 2874 myResultsIterator.close(); 2875 } 2876 myResultsIterator = null; 2877 } 2878 } 2879 2880 private void firePerformanceInfo(RequestDetails theRequest, String theMessage) { 2881 // Only log at debug level since these messages aren't considered important enough 2882 // that we should be cluttering the system log, but they are important to the 2883 // specific query being executed to we'll INFO level them there 2884 ourLog.debug(theMessage); 2885 firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_INFO); 2886 } 2887 2888 private void firePerformanceWarning(RequestDetails theRequest, String theMessage) { 2889 ourLog.warn(theMessage); 2890 firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_WARNING); 2891 } 2892 2893 private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut thePointcut) { 2894 IInterceptorBroadcaster compositeBroadcaster = 2895 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 2896 if (compositeBroadcaster.hasHooks(thePointcut)) { 2897 StorageProcessingMessage message = new StorageProcessingMessage(); 2898 message.setMessage(theMessage); 2899 HookParams params = new HookParams() 2900 .add(RequestDetails.class, theRequest) 2901 .addIfMatchesType(ServletRequestDetails.class, theRequest) 2902 .add(StorageProcessingMessage.class, message); 2903 compositeBroadcaster.callHooks(thePointcut, params); 2904 } 2905 } 2906 2907 public static int getMaximumPageSize() { 2908 if (myMaxPageSizeForTests != null) { 2909 return myMaxPageSizeForTests; 2910 } 2911 return MAXIMUM_PAGE_SIZE; 2912 } 2913 2914 public static void setMaxPageSizeForTest(Integer theTestSize) { 2915 myMaxPageSizeForTests = theTestSize; 2916 } 2917}