change search method for missing parameter names (#3982)

* handle changing in missing parameters

* added changelog

* refactor step1

* refactor step 2

* refactor step 3

* refactor step 3

* more code review fixes

* tra lala

* tra lala

* blah

* more changes

* review fixes

* review point

* more review points

* blah

Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-MacBook-Pro.local>
This commit is contained in:
TipzCM 2022-09-14 09:29:16 -04:00 committed by GitHub
parent 1a3de51cfd
commit ae3faafc9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2740 additions and 1304 deletions

View File

@ -25,7 +25,7 @@ public final class Msg {
/**
* IMPORTANT: Please update the following comment after you add a new code
* Last used code value: 2139
* Last used code value: 2140
*/
private Msg() {}

View File

@ -0,0 +1,8 @@
---
type: fix
issue: 3951
title: "There are now 2 different methods of Missing Fields search.
One that works if Enable Missing Fields Search is enabled,
and one that works if it is not.
These 2 are not compatible together.
"

View File

@ -8,10 +8,8 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.batch.BatchJobsConfig;
import ca.uhn.fhir.jpa.batch.config.BatchConstants;
import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor;
@ -73,11 +71,10 @@ import ca.uhn.fhir.jpa.search.ISynchronousSearchSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.search.SearchStrategyFactory;
import ca.uhn.fhir.jpa.search.SynchronousSearchSvcImpl;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
@ -116,9 +113,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl;
import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl;
import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.config.TermCodeSystemConfig;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl;
@ -133,11 +128,9 @@ import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import ca.uhn.fhir.util.ThreadPoolUtil;
import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.npm.PackageClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -184,7 +177,8 @@ import java.util.Date;
SearchParamConfig.class,
ValidationSupportConfig.class,
Batch2SupportConfig.class,
JpaBulkExportConfig.class
JpaBulkExportConfig.class,
SearchConfig.class
})
public class JpaConfig {
public static final String JPA_VALIDATION_SUPPORT_CHAIN = "myJpaValidationSupportChain";
@ -499,7 +493,7 @@ public class JpaConfig {
@Bean(name = PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER)
@Scope("prototype")
public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider(RequestDetails theRequest, Search theSearch, SearchCoordinatorSvcImpl.SearchTask theSearchTask, ISearchBuilder theSearchBuilder) {
public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider(RequestDetails theRequest, Search theSearch, SearchTask theSearchTask, ISearchBuilder theSearchBuilder) {
return new PersistedJpaSearchFirstPageBundleProvider(theSearch, theSearchTask, theSearchBuilder, theRequest);
}
@ -618,12 +612,6 @@ public class JpaConfig {
return new SearchQueryExecutor(theGeneratedSql, theMaxResultsToFetch);
}
@Bean(name = ISearchBuilder.SEARCH_BUILDER_BEAN_NAME)
@Scope("prototype")
public ISearchBuilder newSearchBuilder(IDao theDao, String theResourceName, Class<? extends IBaseResource> theResourceType, DaoConfig theDaoConfig) {
return new SearchBuilder(theDao, theResourceName, theResourceType);
}
@Bean(name = HISTORY_BUILDER)
@Scope("prototype")
public HistoryBuilder newPersistedJpaSearchFirstPageBundleProvider(@Nullable String theResourceType, @Nullable Long theResourceId, @Nullable Date theRangeStartInclusive, @Nullable Date theRangeEndInclusive) {
@ -641,11 +629,6 @@ public class JpaConfig {
return new IdHelperService();
}
@Bean
public ISearchCoordinatorSvc searchCoordinatorSvc() {
return new SearchCoordinatorSvcImpl();
}
@Bean
public SearchStrategyFactory searchStrategyFactory(@Autowired(required = false) IFulltextSearchSvc theFulltextSvc) {
return new SearchStrategyFactory(myDaoConfig, theFulltextSvc);

View File

@ -0,0 +1,174 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.ExceptionService;
import ca.uhn.fhir.jpa.search.ISynchronousSearchSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.search.SearchStrategyFactory;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.search.builder.sql.SqlObjectFactory;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchContinuationTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTaskParameters;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class SearchConfig {
public static final String SEARCH_TASK = "searchTask";
public static final String CONTINUE_TASK = "continueTask";
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
@Autowired
private SqlObjectFactory mySqlBuilderFactory;
@Autowired
private HibernatePropertiesProvider myDialectProvider;
@Autowired
private ModelConfig myModelConfig;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IResourceTagDao myResourceTagDao;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@Autowired
private IIdHelperService myIdHelperService;
@Autowired
private PlatformTransactionManager myManagedTxManager;
@Autowired
private SearchStrategyFactory mySearchStrategyFactory;
@Autowired
private SearchBuilderFactory mySearchBuilderFactory;
@Autowired
private ISearchResultCacheSvc mySearchResultCacheSvc;
@Autowired
private ISearchCacheSvc mySearchCacheSvc;
@Autowired
private IPagingProvider myPagingProvider;
@Autowired
private BeanFactory myBeanFactory;
@Autowired
private ISynchronousSearchSvc mySynchronousSearchSvc;
@Autowired
private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperService;
@Bean
public ISearchCoordinatorSvc searchCoordinatorSvc() {
return new SearchCoordinatorSvcImpl(
myContext,
myDaoConfig,
myInterceptorBroadcaster,
myManagedTxManager,
mySearchCacheSvc,
mySearchResultCacheSvc,
myDaoRegistry,
mySearchBuilderFactory,
mySynchronousSearchSvc,
myPersistedJpaBundleProviderFactory,
myRequestPartitionHelperService,
mySearchParamRegistry,
mySearchStrategyFactory,
exceptionService(),
myBeanFactory
);
}
@Bean
public ExceptionService exceptionService() {
return new ExceptionService(myContext);
}
@Bean(name = ISearchBuilder.SEARCH_BUILDER_BEAN_NAME)
@Scope("prototype")
public ISearchBuilder newSearchBuilder(IDao theDao, String theResourceName, Class<? extends IBaseResource> theResourceType, DaoConfig theDaoConfig) {
return new SearchBuilder(theDao,
theResourceName,
myDaoConfig,
myEntityManagerFactory,
mySqlBuilderFactory,
myDialectProvider,
myModelConfig,
mySearchParamRegistry,
myPartitionSettings,
myInterceptorBroadcaster,
myResourceTagDao,
myDaoRegistry,
myResourceSearchViewDao,
myContext,
myIdHelperService,
theResourceType
);
}
@Bean(name = SEARCH_TASK)
@Scope("prototype")
public SearchTask createSearchTask(SearchTaskParameters theParams) {
return new SearchTask(theParams,
myManagedTxManager,
myContext,
mySearchStrategyFactory,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myDaoConfig,
mySearchCacheSvc,
myPagingProvider
);
}
@Bean(name = CONTINUE_TASK)
@Scope("prototype")
public SearchContinuationTask createSearchContinuationTask(SearchTaskParameters theParams) {
return new SearchContinuationTask(theParams,
myManagedTxManager,
myContext,
mySearchStrategyFactory,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myDaoConfig,
mySearchCacheSvc,
myPagingProvider,
exceptionService() // singleton
);
}
}

View File

@ -1538,18 +1538,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new MethodNotAllowedException(Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server");
}
if (getConfig().getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
for (List<List<IQueryParameterType>> nextAnds : theParams.values()) {
for (List<? extends IQueryParameterType> nextOrs : nextAnds) {
for (IQueryParameterType next : nextOrs) {
if (next.getMissing() != null) {
throw new MethodNotAllowedException(Msg.code(985) + ":missing modifier is disabled on this server");
}
}
}
}
}
translateListSearchParams(theParams);
notifySearchInterceptors(theParams, theRequest);

View File

@ -53,7 +53,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.SearchBuilder.toPredicateArray;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toPredicateArray;
/**

View File

@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.entity;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
@ -228,7 +228,7 @@ public class Search implements ICachedSearchDetails, Serializable {
public void setFailureMessage(String theFailureMessage) {
myFailureMessage = left(theFailureMessage, FAILURE_MESSAGE_LENGTH);
if (System.getProperty(SearchCoordinatorSvcImpl.UNIT_TEST_CAPTURE_STACK) != null) {
if (System.getProperty(QueryParameterUtils.UNIT_TEST_CAPTURE_STACK) != null) {
myFailureMessage = theFailureMessage;
}
}

View File

@ -103,7 +103,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.SearchBuilder.toPredicateArray;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toPredicateArray;
import static ca.uhn.fhir.util.StringUtil.toUtf8String;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

View File

@ -0,0 +1,24 @@
package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
import javax.annotation.Nonnull;
public class ExceptionService {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchCoordinatorSvcImpl.class);
private final FhirContext myContext;
public ExceptionService(FhirContext theContext) {
myContext = theContext;
}
@Nonnull
public ResourceGoneException newUnknownSearchException(String theUuid) {
ourLog.trace("Client requested unknown paging ID[{}]", theUuid);
String msg = myContext.getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", theUuid);
return new ResourceGoneException(msg);
}
}

View File

@ -41,6 +41,7 @@ import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
@ -384,7 +385,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
@Override
public Integer size() {
ensureSearchEntityLoaded();
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity);
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity);
Integer size = mySearchEntity.getTotalCount();
if (size != null) {

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.jpa.config.JpaConfig;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
@ -42,7 +43,7 @@ public class PersistedJpaBundleProviderFactory {
return (PersistedJpaBundleProvider) retVal;
}
public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(RequestDetails theRequestDetails, Search theSearch, SearchCoordinatorSvcImpl.SearchTask theTask, ISearchBuilder theSearchBuilder) {
public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(RequestDetails theRequestDetails, Search theSearch, SearchTask theTask, ISearchBuilder theSearchBuilder) {
return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean(JpaConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER, theRequestDetails, theSearch, theTask, theSearchBuilder);
}
}

View File

@ -22,9 +22,10 @@ package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.SearchTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
@ -61,7 +62,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
@Nonnull
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
mySearchTask.awaitInitialSync();
@ -121,7 +122,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
Integer size = mySearchTask.awaitInitialSync();
ourLog.trace("size() - Finished waiting for local sync");
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
if (size != null) {
return size;
}

View File

@ -33,13 +33,20 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.search.builder.models.MissingParameterQueryParams;
import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheKey;
import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheLookupResult;
import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderTypeEnum;
import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.BaseQuantityPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.BaseSearchParamPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate;
import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
@ -50,11 +57,13 @@ import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.sql.PredicateBuilderFactory;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -68,7 +77,6 @@ import ca.uhn.fhir.rest.param.CompositeParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.HasParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
@ -94,22 +102,15 @@ import com.healthmarketscience.sqlbuilder.SetOperationQuery;
import com.healthmarketscience.sqlbuilder.Subquery;
import com.healthmarketscience.sqlbuilder.UnionQuery;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.tuple.Triple;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@ -121,29 +122,19 @@ import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.fromOperation;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getChainedPart;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getParamNameWithPrefix;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.split;
public class QueryStack {
private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class);
private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
static {
DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER);
ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix);
}
private final ModelConfig myModelConfig;
private final FhirContext myFhirContext;
@ -355,25 +346,194 @@ public class QueryStack {
}
private Condition createMissingParameterQuery(
MissingParameterQueryParams theParams
) {
if (theParams.getParamType() == RestSearchParameterTypeEnum.COMPOSITE) {
ourLog.error("Cannot create missing parameter query for a composite parameter.");
return null;
} else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
if (isEligibleForContainedResourceSearch(theParams.getQueryParameterTypes())) {
ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search.");
return null;
}
}
// TODO - Change this when we have HFJ_SPIDX_MISSING table
/**
* How we search depends on if the
* {@link DaoConfig#getIndexMissingFields()} property
* is Enabled or Disabled.
*
* If it is, we will use the SP_MISSING values set into the various
* SP_INDX_X tables and search on those ("old" search).
*
* If it is not set, however, we will try and construct a query that
* looks for missing SearchParameters in the SP_IDX_* tables ("new" search).
*
* You cannot mix and match, however (SP_MISSING is not in HASH_IDENTITY information).
* So setting (or unsetting) the IndexMissingFields
* property should always be followed up with a /$reindex call.
*
* ---
*
* Current limitations:
* Checking if a row exists ("new" search) for a given missing field in an SP_INDX_* table
* (ie, :missing=true) is slow when there are many resources in the table. (Defaults to
* a table scan, since HASH_IDENTITY isn't part of the index).
*
* However, the "old" search method was slow for the reverse: when looking for resources
* that do not have a missing field (:missing=false) for much the same reason.
*/
SearchQueryBuilder sqlBuilder = theParams.getSqlBuilder();
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) {
// new search
return createMissingPredicateForUnindexedMissingFields(theParams, sqlBuilder);
} else {
// old search
return createMissingPredicateForIndexedMissingFields(theParams, sqlBuilder);
}
}
/**
* Old way of searching.
* Missing values must be indexed!
*/
private Condition createMissingPredicateForIndexedMissingFields(MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
PredicateBuilderTypeEnum predicateType = null;
Supplier<? extends BaseJoiningPredicateBuilder> supplier = null;
switch (theParams.getParamType()) {
case STRING:
predicateType = PredicateBuilderTypeEnum.STRING;
supplier = () -> sqlBuilder.addStringPredicateBuilder(theParams.getSourceJoinColumn());
break;
case NUMBER:
predicateType = PredicateBuilderTypeEnum.NUMBER;
supplier = () -> sqlBuilder.addNumberPredicateBuilder(theParams.getSourceJoinColumn());
break;
case DATE:
predicateType = PredicateBuilderTypeEnum.DATE;
supplier = () -> sqlBuilder.addDatePredicateBuilder(theParams.getSourceJoinColumn());
break;
case TOKEN:
predicateType = PredicateBuilderTypeEnum.TOKEN;
supplier = () -> sqlBuilder.addTokenPredicateBuilder(theParams.getSourceJoinColumn());
break;
case QUANTITY:
predicateType = PredicateBuilderTypeEnum.QUANTITY;
supplier = () -> sqlBuilder.addQuantityPredicateBuilder(theParams.getSourceJoinColumn());
break;
case REFERENCE:
case URI:
// we expect these values, but the pattern is slightly different;
// see below
break;
case HAS:
case SPECIAL:
predicateType = PredicateBuilderTypeEnum.COORDS;
supplier = () -> sqlBuilder.addCoordsPredicateBuilder(theParams.getSourceJoinColumn());
break;
case COMPOSITE:
default:
break;
}
if (supplier != null) {
BaseSearchParamPredicateBuilder join = (BaseSearchParamPredicateBuilder) createOrReusePredicateBuilder(
predicateType,
theParams.getSourceJoinColumn(),
theParams.getParamName(),
supplier
).getResult();
return join.createPredicateParamMissingForNonReference(
theParams.getResourceType(),
theParams.getParamName(),
theParams.isMissing(),
theParams.getRequestPartitionId()
);
} else {
if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
SearchParamPresentPredicateBuilder join = sqlBuilder.addSearchParamPresentPredicateBuilder(theParams.getSourceJoinColumn());
return join.createPredicateParamMissingForReference(
theParams.getResourceType(),
theParams.getParamName(),
theParams.isMissing(),
theParams.getRequestPartitionId()
);
} else if (theParams.getParamType() == RestSearchParameterTypeEnum.URI) {
UriPredicateBuilder join = sqlBuilder.addUriPredicateBuilder(theParams.getSourceJoinColumn());
return join.createPredicateParamMissingForNonReference(
theParams.getResourceType(),
theParams.getParamName(),
theParams.isMissing(),
theParams.getRequestPartitionId()
);
} else {
// we don't expect to see this
ourLog.error("Invalid param type " + theParams.getParamType().name());
return null;
}
}
}
/**
* New way of searching for missing fields.
* Missing values must not indexed!
*/
private Condition createMissingPredicateForUnindexedMissingFields(MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
ResourceTablePredicateBuilder table = sqlBuilder.getOrCreateResourceTablePredicateBuilder();
ICanMakeMissingParamPredicate innerQuery = PredicateBuilderFactory.createPredicateBuilderForParamType(
theParams.getParamType(),
theParams.getSqlBuilder(),
this
);
return innerQuery.createPredicateParamMissingValue(
new MissingQueryParameterPredicateParams(
table,
theParams.isMissing(),
theParams.getParamName(),
theParams.getRequestPartitionId()
)
);
}
public Condition createPredicateCoords(@Nullable DbColumn theSourceJoinColumn,
String theResourceName,
String theSpnamePrefix,
RuntimeSearchParam theSearchParam,
List<? extends IQueryParameterType> theList,
RequestPartitionId theRequestPartitionId) {
RequestPartitionId theRequestPartitionId,
SearchQueryBuilder theSqlBuilder) {
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.COORDS, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)).getResult();
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.COORDS, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)).getResult();
if (theList.get(0).getMissing() != null) {
return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId);
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition singleCode = predicateBuilder.createPredicateCoords(mySearchParameters, nextOr, theResourceName, theSearchParam, predicateBuilder, theRequestPartitionId);
codePredicates.add(singleCode);
}
return predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
}
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition singleCode = predicateBuilder.createPredicateCoords(mySearchParameters, nextOr, theResourceName, theSearchParam, predicateBuilder, theRequestPartitionId);
codePredicates.add(singleCode);
}
return predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
}
public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
@ -384,34 +544,42 @@ public class QueryStack {
public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
if (theList.get(0).getMissing() != null) {
Boolean missing = theList.get(0).getMissing();
return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, paramName, missing, theRequestPartitionId);
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation);
codePredicates.add(p);
}
Condition predicate = toOrPredicate(codePredicates);
if (!cacheHit) {
predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate);
predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}
return predicate;
}
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation);
codePredicates.add(p);
}
Condition predicate = toOrPredicate(codePredicates);
if (!cacheHit) {
predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate);
predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}
return predicate;
}
private Condition createPredicateFilter(QueryStack theQueryStack3, SearchFilterParser.BaseFilter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
@ -597,39 +765,50 @@ public class QueryStack {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult();
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult();
if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
}
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
if (nextOr instanceof NumberParam) {
NumberParam param = (NumberParam) nextOr;
if (nextOr instanceof NumberParam) {
NumberParam param = (NumberParam) nextOr;
BigDecimal value = param.getValue();
if (value == null) {
continue;
}
BigDecimal value = param.getValue();
if (value == null) {
continue;
SearchFilterParser.CompareOperation operation = theOperation;
if (operation == null) {
operation = toOperation(param.getPrefix());
}
Condition predicate = join.createPredicateNumeric(theResourceName, paramName, operation, value, theRequestPartitionId, nextOr);
codePredicates.add(predicate);
} else {
throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass());
}
SearchFilterParser.CompareOperation operation = theOperation;
if (operation == null) {
operation = toOperation(param.getPrefix());
}
Condition predicate = join.createPredicateNumeric(theResourceName, paramName, operation, value, theRequestPartitionId, nextOr);
codePredicates.add(predicate);
} else {
throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass());
}
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
}
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
}
public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
@ -644,42 +823,52 @@ public class QueryStack {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
if (theList.get(0).getMissing() != null) {
BaseQuantityPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
}
List<QuantityParam> quantityParams = theList
.stream()
.map(t -> QuantityParam.toQuantityParam(t))
.collect(Collectors.toList());
BaseQuantityPredicateBuilder join = null;
boolean normalizedSearchEnabled = myModelConfig.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
if (normalizedSearchEnabled) {
List<QuantityParam> normalizedQuantityParams = quantityParams
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
List<QuantityParam> quantityParams = theList
.stream()
.map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t))
.filter(t -> t != null)
.map(t -> QuantityParam.toQuantityParam(t))
.collect(Collectors.toList());
if (normalizedQuantityParams.size() == quantityParams.size()) {
join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult();
quantityParams = normalizedQuantityParams;
BaseQuantityPredicateBuilder join = null;
boolean normalizedSearchEnabled = myModelConfig.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
if (normalizedSearchEnabled) {
List<QuantityParam> normalizedQuantityParams = quantityParams
.stream()
.map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t))
.filter(t -> t != null)
.collect(Collectors.toList());
if (normalizedQuantityParams.size() == quantityParams.size()) {
join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult();
quantityParams = normalizedQuantityParams;
}
}
}
if (join == null) {
join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
}
if (join == null) {
join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
}
List<Condition> codePredicates = new ArrayList<>();
for (QuantityParam nextOr : quantityParams) {
Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId);
codePredicates.add(singleCode);
}
List<Condition> codePredicates = new ArrayList<>();
for (QuantityParam nextOr : quantityParams) {
Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId);
codePredicates.add(singleCode);
}
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
}
}
public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
@ -708,14 +897,23 @@ public class QueryStack {
throw new InvalidRequestException(Msg.code(1212) + "Invalid operator specified for reference predicate. Supported operators for reference predicate are \"eq\" and \"ne\".");
}
if (theList.get(0).getMissing() != null) {
SearchParamPresentPredicateBuilder join = theSqlBuilder.addSearchParamPresentPredicateBuilder(theSourceJoinColumn);
return join.createPredicateParamMissingForReference(theResourceName, theParamName, theList.get(0).getMissing(), theRequestPartitionId);
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
RestSearchParameterTypeEnum.REFERENCE,
theList,
theParamName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId);
}
ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId);
}
private class ChainElement {
@ -1184,15 +1382,25 @@ public class QueryStack {
String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId,
SearchQueryBuilder theSqlBuilder) {
Boolean isMissing = theList.get(0).getMissing();
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult();
if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
}
StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult();
List<Condition> codePredicates = new ArrayList<>();
for (IQueryParameterType nextOr : theList) {
Condition singleCode = join.createPredicateString(nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation);
@ -1367,13 +1575,23 @@ public class QueryStack {
}
} else {
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
}
TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult();
if (theList.get(0).getMissing() != null) {
return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
}
predicate = tokenJoin.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
join = tokenJoin;
}
@ -1395,14 +1613,25 @@ public class QueryStack {
String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
Boolean isMissing = theList.get(0).getMissing();
if (isMissing != null) {
return createMissingParameterQuery(
new MissingParameterQueryParams(
theSqlBuilder,
theSearchParam.getParamType(),
theList,
paramName,
theResourceName,
theSourceJoinColumn,
theRequestPartitionId
)
);
} else {
UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
if (theList.get(0).getMissing() != null) {
return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails);
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}
Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails);
return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}
public QueryStack newChildQueryFactoryWithFullBuilderReuse() {
@ -1437,9 +1666,7 @@ public class QueryStack {
default:
return createPredicateSearchParameter(theSourceJoinColumn, theResourceName, theParamName, theAndOrParams, theRequest, theRequestPartitionId);
}
}
@Nullable
@ -1494,7 +1721,7 @@ public class QueryStack {
case TOKEN:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
if ("Location.position".equals(nextParamDef.getPath())) {
andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId));
andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, theRequestPartitionId, mySqlBuilder));
} else {
andPredicates.add(createPredicateToken(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId));
}
@ -1519,7 +1746,7 @@ public class QueryStack {
case SPECIAL:
for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
if ("Location.position".equals(nextParamDef.getPath())) {
andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId));
andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, theRequestPartitionId, mySqlBuilder));
}
}
break;
@ -1629,150 +1856,4 @@ public class QueryStack {
return qp;
}
private enum PredicateBuilderTypeEnum {
DATE, COORDS, NUMBER, QUANTITY, REFERENCE, SOURCE, STRING, TOKEN, TAG
}
private static class PredicateBuilderCacheLookupResult<T extends BaseJoiningPredicateBuilder> {
private final boolean myCacheHit;
private final T myResult;
private PredicateBuilderCacheLookupResult(boolean theCacheHit, T theResult) {
myCacheHit = theCacheHit;
myResult = theResult;
}
public boolean isCacheHit() {
return myCacheHit;
}
public T getResult() {
return myResult;
}
}
private static class PredicateBuilderCacheKey {
private final DbColumn myDbColumn;
private final PredicateBuilderTypeEnum myType;
private final String myParamName;
private final int myHashCode;
private PredicateBuilderCacheKey(DbColumn theDbColumn, PredicateBuilderTypeEnum theType, String theParamName) {
myDbColumn = theDbColumn;
myType = theType;
myParamName = theParamName;
myHashCode = new HashCodeBuilder().append(myDbColumn).append(myType).append(myParamName).toHashCode();
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
PredicateBuilderCacheKey that = (PredicateBuilderCacheKey) theO;
return new EqualsBuilder()
.append(myDbColumn, that.myDbColumn)
.append(myType, that.myType)
.append(myParamName, that.myParamName)
.isEquals();
}
@Override
public int hashCode() {
return myHashCode;
}
}
@Nullable
public static Condition toAndPredicate(List<Condition> theAndPredicates) {
List<Condition> andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
if (andPredicates.size() == 0) {
return null;
} else if (andPredicates.size() == 1) {
return andPredicates.get(0);
} else {
return ComboCondition.and(andPredicates.toArray(new Condition[0]));
}
}
@Nullable
public static Condition toOrPredicate(List<Condition> theOrPredicates) {
List<Condition> orPredicates = theOrPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
if (orPredicates.size() == 0) {
return null;
} else if (orPredicates.size() == 1) {
return orPredicates.get(0);
} else {
return ComboCondition.or(orPredicates.toArray(new Condition[0]));
}
}
@Nullable
public static Condition toOrPredicate(Condition... theOrPredicates) {
return toOrPredicate(Arrays.asList(theOrPredicates));
}
@Nullable
public static Condition toAndPredicate(Condition... theAndPredicates) {
return toAndPredicate(Arrays.asList(theAndPredicates));
}
@Nonnull
public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders, boolean theInverse) {
if (theInverse) {
return toNotEqualToOrNotInPredicate(theColumn, theValuePlaceholders);
} else {
return toEqualToOrInPredicate(theColumn, theValuePlaceholders);
}
}
@Nonnull
public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
if (theValuePlaceholders.size() == 1) {
return BinaryCondition.equalTo(theColumn, theValuePlaceholders.get(0));
}
return new InCondition(theColumn, theValuePlaceholders);
}
@Nonnull
public static Condition toNotEqualToOrNotInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
if (theValuePlaceholders.size() == 1) {
return BinaryCondition.notEqualTo(theColumn, theValuePlaceholders.get(0));
}
return new InCondition(theColumn, theValuePlaceholders).setNegate(true);
}
public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) {
SearchFilterParser.CompareOperation retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.getKey(thePrefix);
}
return defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq);
}
public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) {
ParamPrefixEnum retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.get(thePrefix);
}
return defaultIfNull(retVal, ParamPrefixEnum.EQUAL);
}
private static String getChainedPart(String parameter) {
return parameter.substring(parameter.indexOf(".") + 1);
}
public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) {
if (isBlank(theSpnamePrefix))
return theParamName;
return theSpnamePrefix + "." + theParamName;
}
}

View File

@ -49,7 +49,6 @@ import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTag;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
@ -66,12 +65,12 @@ import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.QueryChunker;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.util.SqlQueryList;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
@ -115,10 +114,6 @@ import javax.persistence.Query;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -160,29 +155,7 @@ public class SearchBuilder implements ISearchBuilder {
public static boolean myUseMaxPageSize50ForTest = false;
private final String myResourceName;
private final Class<? extends IBaseResource> myResourceType;
private final IDao myCallingDao;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
protected IResourceTagDao myResourceTagDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@Autowired
private IIdHelperService myIdHelperService;
@Autowired(required = false)
private IFulltextSearchSvc myFulltextSearchSvc;
@Autowired(required = false)
private IElasticsearchSvc myIElasticsearchSvc;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
private List<ResourcePersistentId> myAlsoIncludePids;
private CriteriaBuilder myCriteriaBuilder;
private SearchParameterMap myParams;
@ -192,25 +165,69 @@ public class SearchBuilder implements ISearchBuilder {
private Set<ResourcePersistentId> myPidSet;
private boolean myHasNextIteratorQuery = false;
private RequestPartitionId myRequestPartitionId;
@Autowired
private PartitionSettings myPartitionSettings;
@Autowired
private HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
@Autowired
private SqlObjectFactory mySqlBuilderFactory;
@Autowired
private HibernatePropertiesProvider myDialectProvider;
@Autowired
private ModelConfig myModelConfig;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired(required = false)
private IFulltextSearchSvc myFulltextSearchSvc;
@Autowired(required = false)
private IElasticsearchSvc myIElasticsearchSvc;
private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
private final SqlObjectFactory mySqlBuilderFactory;
private final HibernatePropertiesProvider myDialectProvider;
private final ModelConfig myModelConfig;
private final ISearchParamRegistry mySearchParamRegistry;
private final PartitionSettings myPartitionSettings;
protected final IInterceptorBroadcaster myInterceptorBroadcaster;
protected final IResourceTagDao myResourceTagDao;
private final DaoRegistry myDaoRegistry;
private final IResourceSearchViewDao myResourceSearchViewDao;
private final FhirContext myContext;
private final IIdHelperService myIdHelperService;
private final DaoConfig myDaoConfig;
private final IDao myCallingDao;
/**
* Constructor
*/
public SearchBuilder(IDao theDao, String theResourceName, Class<? extends IBaseResource> theResourceType) {
public SearchBuilder(
IDao theDao,
String theResourceName,
DaoConfig theDaoConfig,
HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory,
SqlObjectFactory theSqlBuilderFactory,
HibernatePropertiesProvider theDialectProvider,
ModelConfig theModelConfig,
ISearchParamRegistry theSearchParamRegistry,
PartitionSettings thePartitionSettings,
IInterceptorBroadcaster theInterceptorBroadcaster,
IResourceTagDao theResourceTagDao,
DaoRegistry theDaoRegistry,
IResourceSearchViewDao theResourceSearchViewDao,
FhirContext theContext,
IIdHelperService theIdHelperService,
Class<? extends IBaseResource> theResourceType
) {
myCallingDao = theDao;
myResourceName = theResourceName;
myResourceType = theResourceType;
myDaoConfig = theDaoConfig;
myEntityManagerFactory = theEntityManagerFactory;
mySqlBuilderFactory = theSqlBuilderFactory;
myDialectProvider = theDialectProvider;
myModelConfig = theModelConfig;
mySearchParamRegistry = theSearchParamRegistry;
myPartitionSettings = thePartitionSettings;
myInterceptorBroadcaster = theInterceptorBroadcaster;
myResourceTagDao = theResourceTagDao;
myDaoRegistry = theDaoRegistry;
myResourceSearchViewDao = theResourceSearchViewDao;
myContext = theContext;
myIdHelperService = theIdHelperService;
}
@Override
@ -1233,7 +1250,7 @@ public class SearchBuilder implements ISearchBuilder {
if (theReverseMode) {
if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) {
pidsToInclude = new HashSet<>(filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude));
pidsToInclude = new HashSet<>(QueryParameterUtils.filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude));
}
}
@ -1452,11 +1469,6 @@ public class SearchBuilder implements ISearchBuilder {
return myResourceName;
}
@VisibleForTesting
public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
myDaoConfig = theDaoConfig;
}
public class IncludesIterator extends BaseIterator<ResourcePersistentId> implements Iterator<ResourcePersistentId> {
private final RequestDetails myRequest;
@ -1765,42 +1777,4 @@ public class SearchBuilder implements ISearchBuilder {
myUseMaxPageSize50ForTest = theIsTest;
}
private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
List<Predicate> lastUpdatedPredicates = new ArrayList<>();
if (theLastUpdated != null) {
if (theLastUpdated.getLowerBoundAsInstant() != null) {
ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
lastUpdatedPredicates.add(predicateLower);
}
if (theLastUpdated.getUpperBoundAsInstant() != null) {
Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
lastUpdatedPredicates.add(predicateUpper);
}
}
return lastUpdatedPredicates;
}
private static List<ResourcePersistentId> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<ResourcePersistentId> thePids) {
if (thePids.isEmpty()) {
return Collections.emptyList();
}
CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTable> from = cq.from(ResourceTable.class);
cq.select(from.get("myId").as(Long.class));
List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(thePids)));
cq.where(SearchBuilder.toPredicateArray(lastUpdatedPredicates));
TypedQuery<Long> query = theEntityManager.createQuery(cq);
return ResourcePersistentId.fromLongList(query.getResultList());
}
public static Predicate[] toPredicateArray(List<Predicate> thePredicates) {
return thePredicates.toArray(new Predicate[0]);
}
}

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.jpa.search.builder;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
/**
* facade over raw hook intererface
*/
public class StorageInterceptorHooksFacade {
private final IInterceptorBroadcaster myInterceptorBroadcaster;
public StorageInterceptorHooksFacade(IInterceptorBroadcaster theInterceptorBroadcaster) {
myInterceptorBroadcaster = theInterceptorBroadcaster;
}
/**
* Interceptor call: STORAGE_PRESEARCH_REGISTERED
*
* @param theRequestDetails
* @param theParams
* @param search
*/
public void callStoragePresearchRegistered(RequestDetails theRequestDetails, SearchParameterMap theParams, Search search) {
HookParams params = new HookParams()
.add(ICachedSearchDetails.class, search)
.add(RequestDetails.class, theRequestDetails)
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
.add(SearchParameterMap.class, theParams);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params);
}
//private IInterceptorBroadcaster myInterceptorBroadcaster;
}

View File

@ -0,0 +1,109 @@
package ca.uhn.fhir.jpa.search.builder.models;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import java.security.InvalidParameterException;
import java.util.List;
public class MissingParameterQueryParams {
/**
* The sql builder
*/
private final SearchQueryBuilder mySqlBuilder;
/**
* The parameter type
*/
private final RestSearchParameterTypeEnum myParamType;
/**
* The list of query parameter types (only needed for validation)
*/
private final List<? extends IQueryParameterType> myQueryParameterTypes;
/**
* The missing boolean value from :missing=true/false
*/
private final boolean myIsMissing;
/**
* The name of the parameter.
*/
private final String myParamName;
/**
* The resource type
*/
private final String myResourceType;
/**
* The column on which to join.
*/
private final DbColumn mySourceJoinColumn;
/**
* The partition id
*/
private final RequestPartitionId myRequestPartitionId;
public MissingParameterQueryParams(
SearchQueryBuilder theSqlBuilder,
RestSearchParameterTypeEnum theParamType,
List<? extends IQueryParameterType> theList,
String theParamName,
String theResourceType,
DbColumn theSourceJoinColumn,
RequestPartitionId theRequestPartitionId
) {
mySqlBuilder = theSqlBuilder;
myParamType = theParamType;
myQueryParameterTypes = theList;
if (theList.isEmpty()) {
// this will never happen
throw new InvalidParameterException(Msg.code(2140)
+ " Invalid search parameter list. Cannot be empty!");
}
myIsMissing = theList.get(0).getMissing();
myParamName = theParamName;
myResourceType = theResourceType;
mySourceJoinColumn = theSourceJoinColumn;
myRequestPartitionId = theRequestPartitionId;
}
public SearchQueryBuilder getSqlBuilder() {
return mySqlBuilder;
}
public RestSearchParameterTypeEnum getParamType() {
return myParamType;
}
public List<? extends IQueryParameterType> getQueryParameterTypes() {
return myQueryParameterTypes;
}
public boolean isMissing() {
return myIsMissing;
}
public String getParamName() {
return myParamName;
}
public String getResourceType() {
return myResourceType;
}
public DbColumn getSourceJoinColumn() {
return mySourceJoinColumn;
}
public ca.uhn.fhir.interceptor.model.RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
}

View File

@ -0,0 +1,51 @@
package ca.uhn.fhir.jpa.search.builder.models;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
public class MissingQueryParameterPredicateParams {
/**
* Base ResourceTable predicate builder
*/
private ResourceTablePredicateBuilder myResourceTablePredicateBuilder;
/**
* The missing boolean.
* True if looking for missing fields.
* False if looking for non-missing fields.
*/
private boolean myIsMissing;
/**
* The Search Parameter Name
*/
private String myParamName;
/**
* The partition id
*/
private RequestPartitionId myRequestPartitionId;
public MissingQueryParameterPredicateParams(ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder theResourceTablePredicateBuilder,
boolean theTheMissing,
String theParamName,
ca.uhn.fhir.interceptor.model.RequestPartitionId theRequestPartitionId) {
myResourceTablePredicateBuilder = theResourceTablePredicateBuilder;
myIsMissing = theTheMissing;
myParamName = theParamName;
myRequestPartitionId = theRequestPartitionId;
}
public ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder getResourceTablePredicateBuilder() {
return myResourceTablePredicateBuilder;
}
public boolean isMissing() {
return myIsMissing;
}
public String getParamName() {
return myParamName;
}
public ca.uhn.fhir.interceptor.model.RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
}

View File

@ -0,0 +1,43 @@
package ca.uhn.fhir.jpa.search.builder.models;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class PredicateBuilderCacheKey {
private final DbColumn myDbColumn;
private final PredicateBuilderTypeEnum myType;
private final String myParamName;
private final int myHashCode;
public PredicateBuilderCacheKey(DbColumn theDbColumn, PredicateBuilderTypeEnum theType, String theParamName) {
myDbColumn = theDbColumn;
myType = theType;
myParamName = theParamName;
myHashCode = new HashCodeBuilder().append(myDbColumn).append(myType).append(myParamName).toHashCode();
}
@Override
public boolean equals(Object theO) {
if (this == theO) {
return true;
}
if (theO == null || getClass() != theO.getClass()) {
return false;
}
PredicateBuilderCacheKey that = (PredicateBuilderCacheKey) theO;
return new EqualsBuilder()
.append(myDbColumn, that.myDbColumn)
.append(myType, that.myType)
.append(myParamName, that.myParamName)
.isEquals();
}
@Override
public int hashCode() {
return myHashCode;
}
}

View File

@ -0,0 +1,21 @@
package ca.uhn.fhir.jpa.search.builder.models;
import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
public class PredicateBuilderCacheLookupResult<T extends BaseJoiningPredicateBuilder> {
private final boolean myCacheHit;
private final T myResult;
public PredicateBuilderCacheLookupResult(boolean theCacheHit, T theResult) {
myCacheHit = theCacheHit;
myResult = theResult;
}
public boolean isCacheHit() {
return myCacheHit;
}
public T getResult() {
return myResult;
}
}

View File

@ -0,0 +1,5 @@
package ca.uhn.fhir.jpa.search.builder.models;
public enum PredicateBuilderTypeEnum {
DATE, COORDS, NUMBER, QUANTITY, REFERENCE, SOURCE, STRING, TOKEN, TAG
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.NotCondition;
@ -34,9 +35,9 @@ import javax.annotation.Nullable;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
@ -64,7 +65,7 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
if (partitionIdPredicate == null) {
return theCondition;
}
return toAndPredicate(partitionIdPredicate, theCondition);
return QueryParameterUtils.toAndPredicate(partitionIdPredicate, theCondition);
}
@ -81,14 +82,14 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
} else if (theRequestPartitionId.hasDefaultPartitionId() && defaultPartitionIsNull) {
List<String> placeholders = generatePlaceholders(theRequestPartitionId.getPartitionIdsWithoutDefault());
UnaryCondition partitionNullPredicate = UnaryCondition.isNull(getPartitionIdColumn());
Condition partitionIdsPredicate = toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
condition = toOrPredicate(partitionNullPredicate, partitionIdsPredicate);
Condition partitionIdsPredicate = QueryParameterUtils.toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
condition = QueryParameterUtils.toOrPredicate(partitionNullPredicate, partitionIdsPredicate);
} else {
List<Integer> partitionIds = theRequestPartitionId.getPartitionIds();
partitionIds = replaceDefaultPartitionIdIfNonNull(getPartitionSettings(), partitionIds);
List<String> placeholders = generatePlaceholders(partitionIds);
condition = toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
condition = QueryParameterUtils.toEqualToOrInPredicate(getPartitionIdColumn(), placeholders);
}
return condition;
} else {
@ -100,7 +101,7 @@ public abstract class BaseJoiningPredicateBuilder extends BasePredicateBuilder {
Validate.notNull(theResourceIds, "theResourceIds must not be null");
// Handle the _id parameter by adding it to the tail
Condition inResourceIds = toEqualToOrInPredicate(getResourceIdColumn(), generatePlaceholders(theResourceIds));
Condition inResourceIds = QueryParameterUtils.toEqualToOrInPredicate(getResourceIdColumn(), generatePlaceholders(theResourceIds));
if (theInverse) {
inResourceIds = new NotCondition(inResourceIds);
}

View File

@ -25,7 +25,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
@ -80,7 +80,7 @@ public abstract class BaseQuantityPredicateBuilder extends BaseSearchParamPredic
SearchFilterParser.CompareOperation operation = theOperation;
if (operation == null && cmpValue != null) {
operation = QueryStack.toOperation(cmpValue);
operation = QueryParameterUtils.toOperation(cmpValue);
}
operation = defaultIfNull(operation, SearchFilterParser.CompareOperation.eq);
Condition numericPredicate = NumberPredicateBuilder.createPredicateNumeric(this, operation, valueValue, myColumnValue, "invalidQuantityPrefix", myFhirContext, theParam);

View File

@ -22,10 +22,15 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.NotCondition;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.UnaryCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;
@ -33,9 +38,11 @@ import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
public abstract class BaseSearchParamPredicateBuilder extends BaseJoiningPredicateBuilder {
public abstract class BaseSearchParamPredicateBuilder
extends BaseJoiningPredicateBuilder
implements ICanMakeMissingParamPredicate {
private final DbColumn myColumnMissing;
private final DbColumn myColumnResType;
@ -81,7 +88,7 @@ public abstract class BaseSearchParamPredicateBuilder extends BaseJoiningPredica
andPredicates.add(hashIdentityPredicate);
andPredicates.add(thePredicate);
return toAndPredicate(andPredicates);
return QueryParameterUtils.toAndPredicate(andPredicates);
}
@Nonnull
@ -98,6 +105,37 @@ public abstract class BaseSearchParamPredicateBuilder extends BaseJoiningPredica
BinaryCondition.equalTo(getMissingColumn(), generatePlaceholder(theMissing))
);
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, condition);
}
@Override
public Condition createPredicateParamMissingValue(MissingQueryParameterPredicateParams theParams) {
SelectQuery subquery = new SelectQuery();
subquery.addCustomColumns(1);
subquery.addFromTable(getTable());
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(
getPartitionSettings(),
theParams.getRequestPartitionId(),
theParams.getResourceTablePredicateBuilder().getResourceType(),
theParams.getParamName()
);
Condition subQueryCondition = ComboCondition.and(
BinaryCondition.equalTo(getResourceIdColumn(),
theParams.getResourceTablePredicateBuilder().getResourceIdColumn()
),
BinaryCondition.equalTo(getColumnHashIdentity(),
generatePlaceholder(hashIdentity))
);
subquery.addCondition(subQueryCondition);
Condition unaryCondition = UnaryCondition.exists(subquery);
if (theParams.isMissing()) {
unaryCondition = new NotCondition(unaryCondition);
}
return combineWithRequestPartitionIdPredicate(theParams.getRequestPartitionId(), unaryCondition);
}
}

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.jpa.search.builder.predicate;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
import com.healthmarketscience.sqlbuilder.Condition;
public interface ICanMakeMissingParamPredicate {
/**
* Creates the condition for searching for a missing field
* for a given SearchParameter type.
*
* Only use if {@link DaoConfig#getIndexMissingFields()} is disabled
*/
Condition createPredicateParamMissingValue(MissingQueryParameterPredicateParams theParams);
}

View File

@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
@ -131,7 +131,7 @@ public class ResourceIdPredicateBuilder extends BasePredicateBuilder {
return queryRootTable.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
}
} else {
return QueryStack.toEqualToOrInPredicate(theSourceJoinColumn, generatePlaceholders(resourceIds), operation == SearchFilterParser.CompareOperation.ne);
return QueryParameterUtils.toEqualToOrInPredicate(theSourceJoinColumn, generatePlaceholders(resourceIds), operation == SearchFilterParser.CompareOperation.ne);
}
}

View File

@ -40,7 +40,9 @@ import ca.uhn.fhir.jpa.dao.BaseStorageDao;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
@ -72,6 +74,9 @@ import com.google.common.collect.Lists;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.NotCondition;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.UnaryCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -91,13 +96,15 @@ import java.util.ListIterator;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.trim;
public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
public class ResourceLinkPredicateBuilder
extends BaseJoiningPredicateBuilder
implements ICanMakeMissingParamPredicate {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceLinkPredicateBuilder.class);
private final DbColumn myColumnSrcType;
@ -138,6 +145,14 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
myQueryStack = theQueryStack;
}
private DbColumn getResourceTypeColumn() {
if (myReversed) {
return myColumnTargetResourceType;
} else {
return myColumnSrcType;
}
}
public DbColumn getColumnSourcePath() {
return myColumnSrcPath;
}
@ -241,13 +256,13 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
Condition targetPidCondition = null;
if (!theTargetPidList.isEmpty()) {
List<String> placeholders = generatePlaceholders(theTargetPidList);
targetPidCondition = toEqualToOrInPredicate(myColumnTargetResourceId, placeholders, theInverse);
targetPidCondition = QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceId, placeholders, theInverse);
}
Condition targetUrlsCondition = null;
if (!theTargetQualifiedUrls.isEmpty()) {
List<String> placeholders = generatePlaceholders(theTargetQualifiedUrls);
targetUrlsCondition = toEqualToOrInPredicate(myColumnTargetResourceUrl, placeholders, theInverse);
targetUrlsCondition = QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceUrl, placeholders, theInverse);
}
Condition joinedCondition;
@ -267,7 +282,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
@Nonnull
public Condition createPredicateSourcePaths(List<String> thePathsToMatch) {
return toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
}
public Condition createPredicateSourcePaths(String theResourceName, String theParamName, List<String> theQualifiers) {
@ -338,7 +353,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
Condition condition = BinaryCondition.equalTo(myColumnTargetResourceType, generatePlaceholder(theReferenceParam.getValue()));
return toAndPredicate(typeCondition, condition);
return QueryParameterUtils.toAndPredicate(typeCondition, condition);
}
boolean foundChainMatch = false;
@ -415,7 +430,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
List<List<IQueryParameterType>> chainParamValues = Collections.singletonList(orValues);
andPredicates.add(childQueryFactory.searchForIdsWithAndOr(myColumnTargetResourceId, subResourceName, chain, chainParamValues, theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE));
orPredicates.add(toAndPredicate(andPredicates));
orPredicates.add(QueryParameterUtils.toAndPredicate(andPredicates));
}
if (candidateTargetTypes.isEmpty()) {
@ -429,14 +444,14 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
// If :not modifier for a token, switch OR with AND in the multi-type case
Condition multiTypePredicate;
if (paramInverted) {
multiTypePredicate = toAndPredicate(orPredicates);
multiTypePredicate = QueryParameterUtils.toAndPredicate(orPredicates);
} else {
multiTypePredicate = toOrPredicate(orPredicates);
multiTypePredicate = QueryParameterUtils.toOrPredicate(orPredicates);
}
List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
return toAndPredicate(pathPredicate, multiTypePredicate);
return QueryParameterUtils.toAndPredicate(pathPredicate, multiTypePredicate);
}
@Nonnull
@ -695,7 +710,7 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
if (theTargetPids != null && theTargetPids.length >= 1) {
// if resource ids are provided, we'll create the predicate
// with ids in or equal to this value
condition = toEqualToOrInPredicate(myColumnTargetResourceId, generatePlaceholders(Arrays.asList(theTargetPids)));
condition = QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceId, generatePlaceholders(Arrays.asList(theTargetPids)));
} else {
// ... otherwise we look for resource types
condition = BinaryCondition.equalTo(myColumnTargetResourceType, generatePlaceholder(theResourceName));
@ -703,10 +718,35 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
if (!theSourceResourceNames.isEmpty()) {
// if source resources are provided, add on predicate for _type operation
Condition typeCondition = toEqualToOrInPredicate(myColumnSrcType, generatePlaceholders(theSourceResourceNames));
condition = toAndPredicate(List.of(condition, typeCondition));
Condition typeCondition = QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcType, generatePlaceholders(theSourceResourceNames));
condition = QueryParameterUtils.toAndPredicate(List.of(condition, typeCondition));
}
return condition;
}
@Override
public Condition createPredicateParamMissingValue(MissingQueryParameterPredicateParams theParams) {
SelectQuery subquery = new SelectQuery();
subquery.addCustomColumns(1);
subquery.addFromTable(getTable());
Condition subQueryCondition = ComboCondition.and(
BinaryCondition.equalTo(getResourceIdColumn(),
theParams.getResourceTablePredicateBuilder().getResourceIdColumn()
),
BinaryCondition.equalTo(getResourceTypeColumn(),
generatePlaceholder(theParams.getResourceTablePredicateBuilder().getResourceType()))
);
subquery.addCondition(subQueryCondition);
Condition unaryCondition = UnaryCondition.exists(subquery);
if (theParams.isMissing()) {
unaryCondition = new NotCondition(unaryCondition);
}
return combineWithRequestPartitionIdPredicate(theParams.getRequestPartitionId(), unaryCondition);
}
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
* #L%
*/
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.Condition;
@ -29,8 +30,8 @@ import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import java.util.Set;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
public class ResourceTablePredicateBuilder extends BaseJoiningPredicateBuilder {
private final DbColumn myColumnResId;
@ -62,10 +63,10 @@ public class ResourceTablePredicateBuilder extends BaseJoiningPredicateBuilder {
if (getResourceType() != null) {
typePredicate = BinaryCondition.equalTo(myColumnResType, generatePlaceholder(getResourceType()));
}
return toAndPredicate(
typePredicate,
UnaryCondition.isNull(myColumnResDeletedAt)
);
return QueryParameterUtils.toAndPredicate(
typePredicate,
UnaryCondition.isNull(myColumnResDeletedAt)
);
}
public DbColumn getLastUpdatedColumn() {
@ -73,7 +74,7 @@ public class ResourceTablePredicateBuilder extends BaseJoiningPredicateBuilder {
}
public Condition createLanguagePredicate(Set<String> theValues, boolean theNegated) {
Condition condition = toEqualToOrInPredicate(myColumnLanguage, generatePlaceholders(theValues));
Condition condition = QueryParameterUtils.toEqualToOrInPredicate(myColumnLanguage, generatePlaceholders(theValues));
if (theNegated) {
condition = new NotCondition(condition);
}

View File

@ -25,8 +25,8 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.StringParam;
@ -82,7 +82,7 @@ public class StringPredicateBuilder extends BaseSearchParamPredicateBuilder {
StringPredicateBuilder theFrom,
SearchFilterParser.CompareOperation operation) {
String rawSearchTerm;
String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
String paramName = QueryParameterUtils.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
if (theParameter instanceof TokenParam) {
TokenParam id = (TokenParam) theParameter;

View File

@ -37,7 +37,7 @@ import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -67,9 +67,9 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -136,7 +136,7 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
final List<FhirVersionIndependentConcept> codes = new ArrayList<>();
String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
String paramName = QueryParameterUtils.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
SearchFilterParser.CompareOperation operation = theOperation;
@ -248,7 +248,7 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
Condition hashIdentityPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hashIdentity));
Condition hashValuePredicate = createPredicateOrList(theResourceName, paramName, sortedCodesList, false);
predicate = toAndPredicate(hashIdentityPredicate, hashValuePredicate);
predicate = QueryParameterUtils.toAndPredicate(hashIdentityPredicate, hashValuePredicate);
} else {
@ -383,7 +383,7 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
if (!haveMultipleColumns && conditions.length > 1) {
List<Long> values = Arrays.asList(hashes);
return toEqualToOrInPredicate(columns[0], generatePlaceholders(values), !theWantEquals);
return QueryParameterUtils.toEqualToOrInPredicate(columns[0], generatePlaceholders(values), !theWantEquals);
}
for (int i = 0; i < conditions.length; i++) {
@ -396,9 +396,9 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
}
if (conditions.length > 1) {
if (theWantEquals) {
return toOrPredicate(conditions);
return QueryParameterUtils.toOrPredicate(conditions);
} else {
return toAndPredicate(conditions);
return QueryParameterUtils.toAndPredicate(conditions);
}
} else {
return conditions[0];

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.model.api.IQueryParameterType;
@ -47,7 +48,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftAndRightMatchLikeExpression;
import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftMatchLikeExpression;
import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createRightMatchLikeExpression;
@ -127,7 +128,7 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder {
continue;
}
Condition uriPredicate = toEqualToOrInPredicate(myColumnUri, generatePlaceholders(toFind));
Condition uriPredicate = QueryParameterUtils.toEqualToOrInPredicate(myColumnUri, generatePlaceholders(toFind));
Condition hashAndUriPredicate = combineWithHashIdentityPredicate(getResourceType(), theParamName, uriPredicate);
codePredicates.add(hashAndUriPredicate);

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.jpa.search.builder.sql;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate;
import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PredicateBuilderFactory {
private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderFactory.class);
public static ICanMakeMissingParamPredicate createPredicateBuilderForParamType(
RestSearchParameterTypeEnum theParamType,
SearchQueryBuilder theBuilder,
QueryStack theQueryStack
) {
switch (theParamType) {
case NUMBER:
return createNumberPredicateBuilder(theBuilder);
case DATE:
return createDatePredicateBuilder(theBuilder);
case STRING:
return createStringPredicateBuilder(theBuilder);
case TOKEN:
return createTokenPredicateBuilder(theBuilder);
case QUANTITY:
return createQuantityPredicateBuilder(theBuilder);
case URI:
return createUriPredicateBuilder(theBuilder);
case REFERENCE:
return createReferencePredicateBuilder(theQueryStack, theBuilder);
case HAS:
case SPECIAL:
return createCoordsPredicateBuilder(theBuilder);
case COMPOSITE:
default:
// we don't expect to see this
ourLog.error("Invalid param type " + theParamType.name());
return null;
}
}
private static StringPredicateBuilder createStringPredicateBuilder(SearchQueryBuilder theBuilder) {
StringPredicateBuilder sp = theBuilder.getSqlBuilderFactory().stringIndexTable(theBuilder);
return sp;
}
private static NumberPredicateBuilder createNumberPredicateBuilder(SearchQueryBuilder theBuilder) {
NumberPredicateBuilder np = theBuilder.getSqlBuilderFactory().numberIndexTable(theBuilder);
return np;
}
private static QuantityPredicateBuilder createQuantityPredicateBuilder(SearchQueryBuilder theBuilder) {
QuantityPredicateBuilder qp = theBuilder.getSqlBuilderFactory().quantityIndexTable(theBuilder);
return qp;
}
private static CoordsPredicateBuilder createCoordsPredicateBuilder(SearchQueryBuilder theBuilder) {
CoordsPredicateBuilder cp = theBuilder.getSqlBuilderFactory().coordsPredicateBuilder(theBuilder);
return cp;
}
private static TokenPredicateBuilder createTokenPredicateBuilder(SearchQueryBuilder theBuilder) {
TokenPredicateBuilder tp = theBuilder.getSqlBuilderFactory().tokenIndexTable(theBuilder);
return tp;
}
private static DatePredicateBuilder createDatePredicateBuilder(SearchQueryBuilder theBuilder) {
DatePredicateBuilder dp = theBuilder.getSqlBuilderFactory().dateIndexTable(theBuilder);
return dp;
}
private static UriPredicateBuilder createUriPredicateBuilder(SearchQueryBuilder theBuilder) {
UriPredicateBuilder up = theBuilder.getSqlBuilderFactory().uriIndexTable(theBuilder);
return up;
}
private static ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack, SearchQueryBuilder theBuilder) {
ResourceLinkPredicateBuilder retVal = theBuilder.getSqlBuilderFactory().referenceIndexTable(theQueryStack, theBuilder, false);
return retVal;
}
}

View File

@ -305,6 +305,9 @@ public class SearchQueryBuilder {
return retVal;
}
public SqlObjectFactory getSqlBuilderFactory() {
return mySqlBuilderFactory;
}
public ResourceIdPredicateBuilder newResourceIdBuilder() {
return mySqlBuilderFactory.resourceId(this);

View File

@ -0,0 +1,85 @@
package ca.uhn.fhir.jpa.search.builder.tasks;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.ExceptionService;
import ca.uhn.fhir.jpa.search.SearchStrategyFactory;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
public class SearchContinuationTask extends SearchTask {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchContinuationTask.class);
private final ExceptionService myExceptionSvc;
public SearchContinuationTask(
SearchTaskParameters theCreationParams,
PlatformTransactionManager theManagedTxManager,
FhirContext theContext,
SearchStrategyFactory theSearchStrategyFactory,
IInterceptorBroadcaster theInterceptorBroadcaster,
SearchBuilderFactory theSearchBuilderFactory,
ISearchResultCacheSvc theSearchResultCacheSvc,
DaoConfig theDaoConfig,
ISearchCacheSvc theSearchCacheSvc,
IPagingProvider thePagingProvider,
ExceptionService theExceptionSvc
) {
super(
theCreationParams,
theManagedTxManager,
theContext,
theSearchStrategyFactory,
theInterceptorBroadcaster,
theSearchBuilderFactory,
theSearchResultCacheSvc,
theDaoConfig,
theSearchCacheSvc,
thePagingProvider
);
myExceptionSvc = theExceptionSvc;
}
@Override
public Void call() {
try {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.afterPropertiesSet();
txTemplate.execute(t -> {
List<ResourcePersistentId> previouslyAddedResourcePids = mySearchResultCacheSvc.fetchAllResultPids(getSearch());
if (previouslyAddedResourcePids == null) {
throw myExceptionSvc.newUnknownSearchException(getSearch().getUuid());
}
ourLog.trace("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid());
setPreviouslyAddedResourcePids(previouslyAddedResourcePids);
return null;
});
} catch (Throwable e) {
ourLog.error("Failure processing search", e);
getSearch().setFailureMessage(e.getMessage());
getSearch().setStatus(SearchStatusEnum.FAILED);
if (e instanceof BaseServerResponseException) {
getSearch().setFailureCode(((BaseServerResponseException) e).getStatusCode());
}
saveSearch();
return null;
}
return super.call();
}
}

View File

@ -0,0 +1,666 @@
package ca.uhn.fhir.jpa.search.builder.tasks;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.dao.IResultIterator;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.ExceptionService;
import ca.uhn.fhir.jpa.search.SearchStrategyFactory;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.util.SearchParameterMapCalculator;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.util.AsyncUtil;
import ca.uhn.fhir.util.StopWatch;
import co.elastic.apm.api.ElasticApm;
import co.elastic.apm.api.Span;
import co.elastic.apm.api.Transaction;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static ca.uhn.fhir.jpa.util.QueryParameterUtils.UNIT_TEST_CAPTURE_STACK;
import static ca.uhn.fhir.jpa.util.SearchParameterMapCalculator.isWantCount;
import static ca.uhn.fhir.jpa.util.SearchParameterMapCalculator.isWantOnlyCount;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* A search task is a Callable task that runs in
* a thread pool to handle an individual search. One instance
* is created for any requested search and runs from the
* beginning to the end of the search.
* <p>
* Understand:
* This class executes in its own thread separate from the
* web server client thread that made the request. We do that
* so that we can return to the client as soon as possible,
* but keep the search going in the background (and have
* the next page of results ready to go when the client asks).
*/
public class SearchTask implements Callable<Void> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchTask.class);
private final SearchParameterMap myParams;
private final IDao myCallingDao;
private final String myResourceType;
private final ArrayList<ResourcePersistentId> mySyncedPids = new ArrayList<>();
private final CountDownLatch myInitialCollectionLatch = new CountDownLatch(1);
private final CountDownLatch myCompletionLatch;
private final ArrayList<ResourcePersistentId> myUnsyncedPids = new ArrayList<>();
private final RequestDetails myRequest;
private final RequestPartitionId myRequestPartitionId;
private final SearchRuntimeDetails mySearchRuntimeDetails;
private final Transaction myParentTransaction;
private Search mySearch;
private boolean myAbortRequested;
private int myCountSavedTotal = 0;
private int myCountSavedThisPass = 0;
private int myCountBlockedThisPass = 0;
private boolean myAdditionalPrefetchThresholdsRemaining;
private List<ResourcePersistentId> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch;
private final Consumer<String> myOnRemove;
private final int mySyncSize;
private final Integer myLoadingThrottleForUnitTests;
private boolean myCustomIsolationSupported;
// injected beans
protected final PlatformTransactionManager myManagedTxManager;
protected final FhirContext myContext;
private final IInterceptorBroadcaster myInterceptorBroadcaster;
private final SearchBuilderFactory mySearchBuilderFactory;
protected final ISearchResultCacheSvc mySearchResultCacheSvc;
private final DaoConfig myDaoConfig;
private final ISearchCacheSvc mySearchCacheSvc;
private final IPagingProvider myPagingProvider;
/**
* Constructor
*/
public SearchTask(
SearchTaskParameters theCreationParams,
PlatformTransactionManager theManagedTxManager,
FhirContext theContext,
SearchStrategyFactory theSearchStrategyFactory,
IInterceptorBroadcaster theInterceptorBroadcaster,
SearchBuilderFactory theSearchBuilderFactory,
ISearchResultCacheSvc theSearchResultCacheSvc,
DaoConfig theDaoConfig,
ISearchCacheSvc theSearchCacheSvc,
IPagingProvider thePagingProvider
) {
// beans
myManagedTxManager = theManagedTxManager;
myContext = theContext;
myInterceptorBroadcaster = theInterceptorBroadcaster;
mySearchBuilderFactory = theSearchBuilderFactory;
mySearchResultCacheSvc = theSearchResultCacheSvc;
myDaoConfig = theDaoConfig;
mySearchCacheSvc = theSearchCacheSvc;
myPagingProvider = thePagingProvider;
// values
myOnRemove = theCreationParams.OnRemove;
mySearch = theCreationParams.Search;
myCallingDao = theCreationParams.CallingDao;
myParams = theCreationParams.Params;
myResourceType = theCreationParams.ResourceType;
myRequest = theCreationParams.Request;
myCompletionLatch = new CountDownLatch(1);
mySyncSize = theCreationParams.SyncSize;
myLoadingThrottleForUnitTests = theCreationParams.getLoadingThrottleForUnitTests();
mySearchRuntimeDetails = new SearchRuntimeDetails(myRequest, mySearch.getUuid());
mySearchRuntimeDetails.setQueryString(myParams.toNormalizedQueryString(myCallingDao.getContext()));
myRequestPartitionId = theCreationParams.RequestPartitionId;
myParentTransaction = ElasticApm.currentTransaction();
if (myManagedTxManager instanceof JpaTransactionManager) {
JpaDialect jpaDialect = ((JpaTransactionManager) myManagedTxManager).getJpaDialect();
if (jpaDialect instanceof HibernateJpaDialect) {
myCustomIsolationSupported = true;
}
}
if (!myCustomIsolationSupported) {
ourLog.warn("JPA dialect does not support transaction isolation! This can have an impact on search performance.");
}
}
/**
* This method is called by the server HTTP thread, and
* will block until at least one page of results have been
* fetched from the DB, and will never block after that.
*/
public Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync");
do {
ourLog.trace("Search {} aborted: {}", getSearch().getUuid(), !isNotAborted());
if (AsyncUtil.awaitLatchAndThrowInternalErrorExceptionOnInterrupt(getInitialCollectionLatch(), 250L, TimeUnit.MILLISECONDS)) {
break;
}
} while (getSearch().getStatus() == SearchStatusEnum.LOADING);
ourLog.trace("Initial sync completed");
return getSearch().getTotalCount();
}
public Search getSearch() {
return mySearch;
}
public CountDownLatch getInitialCollectionLatch() {
return myInitialCollectionLatch;
}
public void setPreviouslyAddedResourcePids(List<ResourcePersistentId> thePreviouslyAddedResourcePids) {
myPreviouslyAddedResourcePids = thePreviouslyAddedResourcePids;
myCountSavedTotal = myPreviouslyAddedResourcePids.size();
}
private ISearchBuilder newSearchBuilder() {
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(myResourceType).getImplementingClass();
return mySearchBuilderFactory.newSearchBuilder(myCallingDao, myResourceType, resourceTypeClass);
}
@Nonnull
public List<ResourcePersistentId> getResourcePids(int theFromIndex, int theToIndex) {
ourLog.debug("Requesting search PIDs from {}-{}", theFromIndex, theToIndex);
boolean keepWaiting;
do {
synchronized (mySyncedPids) {
ourLog.trace("Search status is {}", mySearch.getStatus());
boolean haveEnoughResults = mySyncedPids.size() >= theToIndex;
if (!haveEnoughResults) {
switch (mySearch.getStatus()) {
case LOADING:
keepWaiting = true;
break;
case PASSCMPLET:
/*
* If we get here, it means that the user requested resources that crossed the
* current pre-fetch boundary. For example, if the prefetch threshold is 50 and the
* user has requested resources 0-60, then they would get 0-50 back but the search
* coordinator would then stop searching.SearchCoordinatorSvcImplTest
*/
keepWaiting = false;
break;
case FAILED:
case FINISHED:
case GONE:
default:
keepWaiting = false;
break;
}
} else {
keepWaiting = false;
}
}
if (keepWaiting) {
ourLog.info("Waiting as we only have {} results - Search status: {}", mySyncedPids.size(), mySearch.getStatus());
AsyncUtil.sleep(500L);
}
} while (keepWaiting);
ourLog.debug("Proceeding, as we have {} results", mySyncedPids.size());
ArrayList<ResourcePersistentId> retVal = new ArrayList<>();
synchronized (mySyncedPids) {
QueryParameterUtils.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
int toIndex = theToIndex;
if (mySyncedPids.size() < toIndex) {
toIndex = mySyncedPids.size();
}
for (int i = theFromIndex; i < toIndex; i++) {
retVal.add(mySyncedPids.get(i));
}
}
ourLog.trace("Done syncing results - Wanted {}-{} and returning {} of {}", theFromIndex, theToIndex, retVal.size(), mySyncedPids.size());
return retVal;
}
public void saveSearch() {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theArg0) {
doSaveSearch();
}
});
}
private void saveUnsynced(final IResultIterator theResultIter) {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theArg0) {
if (mySearch.getId() == null) {
doSaveSearch();
}
ArrayList<ResourcePersistentId> unsyncedPids = myUnsyncedPids;
int countBlocked = 0;
// Interceptor call: STORAGE_PREACCESS_RESOURCES
// This can be used to remove results from the search result details before
// the user has a chance to know that they were in the results
if (mySearchRuntimeDetails.getRequestDetails() != null && unsyncedPids.isEmpty() == false) {
JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(unsyncedPids, () -> newSearchBuilder());
HookParams params = new HookParams()
.add(IPreResourceAccessDetails.class, accessDetails)
.add(RequestDetails.class, mySearchRuntimeDetails.getRequestDetails())
.addIfMatchesType(ServletRequestDetails.class, mySearchRuntimeDetails.getRequestDetails());
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
for (int i = unsyncedPids.size() - 1; i >= 0; i--) {
if (accessDetails.isDontReturnResourceAtIndex(i)) {
unsyncedPids.remove(i);
myCountBlockedThisPass++;
myCountSavedTotal++;
countBlocked++;
}
}
}
// Actually store the results in the query cache storage
myCountSavedTotal += unsyncedPids.size();
myCountSavedThisPass += unsyncedPids.size();
mySearchResultCacheSvc.storeResults(mySearch, mySyncedPids, unsyncedPids);
synchronized (mySyncedPids) {
int numSyncedThisPass = unsyncedPids.size();
ourLog.trace("Syncing {} search results - Have more: {}", numSyncedThisPass, theResultIter.hasNext());
mySyncedPids.addAll(unsyncedPids);
unsyncedPids.clear();
if (theResultIter.hasNext() == false) {
int skippedCount = theResultIter.getSkippedCount();
int nonSkippedCount = theResultIter.getNonSkippedCount();
int totalFetched = skippedCount + myCountSavedThisPass + myCountBlockedThisPass;
ourLog.trace("MaxToFetch[{}] SkippedCount[{}] CountSavedThisPass[{}] CountSavedThisTotal[{}] AdditionalPrefetchRemaining[{}]", myMaxResultsToFetch, skippedCount, myCountSavedThisPass, myCountSavedTotal, myAdditionalPrefetchThresholdsRemaining);
if (nonSkippedCount == 0 || (myMaxResultsToFetch != null && totalFetched < myMaxResultsToFetch)) {
ourLog.trace("Setting search status to FINISHED");
mySearch.setStatus(SearchStatusEnum.FINISHED);
mySearch.setTotalCount(myCountSavedTotal - countBlocked);
} else if (myAdditionalPrefetchThresholdsRemaining) {
ourLog.trace("Setting search status to PASSCMPLET");
mySearch.setStatus(SearchStatusEnum.PASSCMPLET);
mySearch.setSearchParameterMap(myParams);
} else {
ourLog.trace("Setting search status to FINISHED");
mySearch.setStatus(SearchStatusEnum.FINISHED);
mySearch.setTotalCount(myCountSavedTotal - countBlocked);
}
}
}
mySearch.setNumFound(myCountSavedTotal);
mySearch.setNumBlocked(mySearch.getNumBlocked() + countBlocked);
int numSynced;
synchronized (mySyncedPids) {
numSynced = mySyncedPids.size();
}
if (myDaoConfig.getCountSearchResultsUpTo() == null ||
myDaoConfig.getCountSearchResultsUpTo() <= 0 ||
myDaoConfig.getCountSearchResultsUpTo() <= numSynced) {
myInitialCollectionLatch.countDown();
}
doSaveSearch();
ourLog.trace("saveUnsynced() - pre-commit");
}
});
ourLog.trace("saveUnsynced() - post-commit");
}
public boolean isNotAborted() {
return myAbortRequested == false;
}
public void markComplete() {
myCompletionLatch.countDown();
}
public CountDownLatch getCompletionLatch() {
return myCompletionLatch;
}
/**
* Request that the task abort as soon as possible
*/
public void requestImmediateAbort() {
myAbortRequested = true;
}
/**
* This is the method which actually performs the search.
* It is called automatically by the thread pool.
*/
@Override
public Void call() {
StopWatch sw = new StopWatch();
Span span = myParentTransaction.startSpan("db", "query", "search");
span.setName("FHIR Database Search");
try {
// Create an initial search in the DB and give it an ID
saveSearch();
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
if (myCustomIsolationSupported) {
txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
}
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theStatus) {
doSearch();
}
});
mySearchRuntimeDetails.setSearchStatus(mySearch.getStatus());
if (mySearch.getStatus() == SearchStatusEnum.FINISHED) {
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_COMPLETE, params);
} else {
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_PASS_COMPLETE, params);
}
ourLog.trace("Have completed search for [{}{}] and found {} resources in {}ms - Status is {}", mySearch.getResourceType(), mySearch.getSearchQueryString(), mySyncedPids.size(), sw.getMillis(), mySearch.getStatus());
} catch (Throwable t) {
/*
* Don't print a stack trace for client errors (i.e. requests that
* aren't valid because the client screwed up).. that's just noise
* in the logs and who needs that.
*/
boolean logged = false;
if (t instanceof BaseServerResponseException) {
BaseServerResponseException exception = (BaseServerResponseException) t;
if (exception.getStatusCode() >= 400 && exception.getStatusCode() < 500) {
logged = true;
ourLog.warn("Failed during search due to invalid request: {}", t.toString());
}
}
if (!logged) {
ourLog.error("Failed during search loading after {}ms", sw.getMillis(), t);
}
myUnsyncedPids.clear();
Throwable rootCause = ExceptionUtils.getRootCause(t);
rootCause = defaultIfNull(rootCause, t);
String failureMessage = rootCause.getMessage();
int failureCode = InternalErrorException.STATUS_CODE;
if (t instanceof BaseServerResponseException) {
failureCode = ((BaseServerResponseException) t).getStatusCode();
}
if (System.getProperty(UNIT_TEST_CAPTURE_STACK) != null) {
failureMessage += "\nStack\n" + ExceptionUtils.getStackTrace(rootCause);
}
mySearch.setFailureMessage(failureMessage);
mySearch.setFailureCode(failureCode);
mySearch.setStatus(SearchStatusEnum.FAILED);
mySearchRuntimeDetails.setSearchStatus(mySearch.getStatus());
HookParams params = new HookParams()
.add(RequestDetails.class, myRequest)
.addIfMatchesType(ServletRequestDetails.class, myRequest)
.add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FAILED, params);
saveSearch();
span.captureException(t);
} finally {
myOnRemove.accept(mySearch.getUuid());
myInitialCollectionLatch.countDown();
markComplete();
span.end();
}
return null;
}
private void doSaveSearch() {
Search newSearch = mySearchCacheSvc.save(mySearch);
// mySearchDao.save is not supposed to return null, but in unit tests
// it can if the mock search dao isn't set up to handle that
if (newSearch != null) {
mySearch = newSearch;
}
}
/**
* This method actually creates the database query to perform the
* search, and starts it.
*/
private void doSearch() {
/*
* If the user has explicitly requested a _count, perform a
*
* SELECT COUNT(*) ....
*
* before doing anything else.
*/
boolean myParamWantOnlyCount = isWantOnlyCount(myParams);
boolean myParamOrDefaultWantCount = nonNull(myParams.getSearchTotalMode()) ? isWantCount(myParams) : SearchParameterMapCalculator.isWantCount(myDaoConfig.getDefaultTotalMode());
if (myParamWantOnlyCount || myParamOrDefaultWantCount) {
ourLog.trace("Performing count");
ISearchBuilder sb = newSearchBuilder();
/*
* createCountQuery
* NB: (see createQuery below)
* Because FulltextSearchSvcImpl will (internally)
* mutate the myParams (searchmap),
* (specifically removing the _content and _text filters)
* we will have to clone those parameters here so that
* the "correct" params are used in createQuery below
*/
Long count = sb.createCountQuery(myParams.clone(), mySearch.getUuid(), myRequest, myRequestPartitionId);
ourLog.trace("Got count {}", count);
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@Nonnull TransactionStatus theArg0) {
mySearch.setTotalCount(count.intValue());
if (myParamWantOnlyCount) {
mySearch.setStatus(SearchStatusEnum.FINISHED);
}
doSaveSearch();
}
});
if (myParamWantOnlyCount) {
return;
}
}
ourLog.trace("Done count");
ISearchBuilder sb = newSearchBuilder();
/*
* Figure out how many results we're actually going to fetch from the
* database in this pass. This calculation takes into consideration the
* "pre-fetch thresholds" specified in DaoConfig#getSearchPreFetchThresholds()
* as well as the value of the _count parameter.
*/
int currentlyLoaded = defaultIfNull(mySearch.getNumFound(), 0);
int minWanted = 0;
if (myParams.getCount() != null) {
minWanted = myParams.getCount();
minWanted = Math.min(minWanted, myPagingProvider.getMaximumPageSize());
minWanted += currentlyLoaded;
}
for (Iterator<Integer> iter = myDaoConfig.getSearchPreFetchThresholds().iterator(); iter.hasNext(); ) {
int next = iter.next();
if (next != -1 && next <= currentlyLoaded) {
continue;
}
if (next == -1) {
sb.setMaxResultsToFetch(null);
} else {
myMaxResultsToFetch = Math.max(next, minWanted);
sb.setMaxResultsToFetch(myMaxResultsToFetch);
}
if (iter.hasNext()) {
myAdditionalPrefetchThresholdsRemaining = true;
}
// If we get here's we've found an appropriate threshold
break;
}
/*
* Provide any PID we loaded in previous search passes to the
* SearchBuilder so that we don't get duplicates coming from running
* the same query again.
*
* We could possibly accomplish this in a different way by using sorted
* results in our SQL query and specifying an offset. I don't actually
* know if that would be faster or not. At some point should test this
* idea.
*/
if (myPreviouslyAddedResourcePids != null) {
sb.setPreviouslyAddedResourcePids(myPreviouslyAddedResourcePids);
mySyncedPids.addAll(myPreviouslyAddedResourcePids);
}
/*
* createQuery
* Construct the SQL query we'll be sending to the database
*
* NB: (See createCountQuery above)
* We will pass the original myParams here (not a copy)
* because we actually _want_ the mutation of the myParams to happen.
* Specifically because SearchBuilder itself will _expect_
* not to have these parameters when dumping back
* to our DB.
*
* This is an odd implementation behaviour, but the change
* for this will require a lot more handling at higher levels
*/
try (IResultIterator resultIterator = sb.createQuery(myParams, mySearchRuntimeDetails, myRequest, myRequestPartitionId)) {
assert (resultIterator != null);
/*
* The following loop actually loads the PIDs of the resources
* matching the search off of the disk and into memory. After
* every X results, we commit to the HFJ_SEARCH table.
*/
int syncSize = mySyncSize;
while (resultIterator.hasNext()) {
myUnsyncedPids.add(resultIterator.next());
boolean shouldSync = myUnsyncedPids.size() >= syncSize;
if (myDaoConfig.getCountSearchResultsUpTo() != null &&
myDaoConfig.getCountSearchResultsUpTo() > 0 &&
myDaoConfig.getCountSearchResultsUpTo() < myUnsyncedPids.size()) {
shouldSync = false;
}
if (myUnsyncedPids.size() > 50000) {
shouldSync = true;
}
// If no abort was requested, bail out
Validate.isTrue(isNotAborted(), "Abort has been requested");
if (shouldSync) {
saveUnsynced(resultIterator);
}
if (myLoadingThrottleForUnitTests != null) {
AsyncUtil.sleep(myLoadingThrottleForUnitTests);
}
}
// If no abort was requested, bail out
Validate.isTrue(isNotAborted(), "Abort has been requested");
saveUnsynced(resultIterator);
} catch (IOException e) {
ourLog.error("IO failure during database access", e);
throw new InternalErrorException(Msg.code(1166) + e);
}
}
}

View File

@ -0,0 +1,48 @@
package ca.uhn.fhir.jpa.search.builder.tasks;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import java.util.function.Consumer;
public class SearchTaskParameters {
// parameters
public ca.uhn.fhir.jpa.entity.Search Search;
public IDao CallingDao;
public SearchParameterMap Params;
public String ResourceType;
public RequestDetails Request;
public ca.uhn.fhir.interceptor.model.RequestPartitionId RequestPartitionId;
public Consumer<String> OnRemove;
public int SyncSize;
private Integer myLoadingThrottleForUnitTests;
public SearchTaskParameters(ca.uhn.fhir.jpa.entity.Search theSearch,
IDao theCallingDao,
SearchParameterMap theParams,
String theResourceType,
RequestDetails theRequest,
ca.uhn.fhir.interceptor.model.RequestPartitionId theRequestPartitionId,
Consumer<String> theOnRemove,
int theSyncSize
) {
Search = theSearch;
CallingDao = theCallingDao;
Params = theParams;
ResourceType = theResourceType;
Request = theRequest;
RequestPartitionId = theRequestPartitionId;
OnRemove = theOnRemove;
SyncSize = theSyncSize;
}
public Integer getLoadingThrottleForUnitTests() {
return myLoadingThrottleForUnitTests;
}
public void setLoadingThrottleForUnitTests(Integer theLoadingThrottleForUnitTests) {
myLoadingThrottleForUnitTests = theLoadingThrottleForUnitTests;
}
}

View File

@ -0,0 +1,229 @@
package ca.uhn.fhir.jpa.util;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.InCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class QueryParameterUtils {
private static final Logger ourLog = LoggerFactory.getLogger(QueryParameterUtils.class);
public static final int DEFAULT_SYNC_SIZE = 250;
public static final String UNIT_TEST_CAPTURE_STACK = "unit_test_capture_stack";
private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
static {
DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE);
compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER);
ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix);
}
@Nullable
public static Condition toAndPredicate(List<Condition> theAndPredicates) {
List<Condition> andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
if (andPredicates.size() == 0) {
return null;
} else if (andPredicates.size() == 1) {
return andPredicates.get(0);
} else {
return ComboCondition.and(andPredicates.toArray(new Condition[0]));
}
}
@Nullable
public static Condition toOrPredicate(List<Condition> theOrPredicates) {
List<Condition> orPredicates = theOrPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
if (orPredicates.size() == 0) {
return null;
} else if (orPredicates.size() == 1) {
return orPredicates.get(0);
} else {
return ComboCondition.or(orPredicates.toArray(new Condition[0]));
}
}
@Nullable
public static Condition toOrPredicate(Condition... theOrPredicates) {
return toOrPredicate(Arrays.asList(theOrPredicates));
}
@Nullable
public static Condition toAndPredicate(Condition... theAndPredicates) {
return toAndPredicate(Arrays.asList(theAndPredicates));
}
@Nonnull
public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders, boolean theInverse) {
if (theInverse) {
return toNotEqualToOrNotInPredicate(theColumn, theValuePlaceholders);
} else {
return toEqualToOrInPredicate(theColumn, theValuePlaceholders);
}
}
@Nonnull
public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
if (theValuePlaceholders.size() == 1) {
return BinaryCondition.equalTo(theColumn, theValuePlaceholders.get(0));
}
return new InCondition(theColumn, theValuePlaceholders);
}
@Nonnull
public static Condition toNotEqualToOrNotInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
if (theValuePlaceholders.size() == 1) {
return BinaryCondition.notEqualTo(theColumn, theValuePlaceholders.get(0));
}
return new InCondition(theColumn, theValuePlaceholders).setNegate(true);
}
public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) {
SearchFilterParser.CompareOperation retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.getKey(thePrefix);
}
return ObjectUtils.defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq);
}
public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) {
ParamPrefixEnum retVal = null;
if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) {
retVal = ourCompareOperationToParamPrefix.get(thePrefix);
}
return ObjectUtils.defaultIfNull(retVal, ParamPrefixEnum.EQUAL);
}
public static String getChainedPart(String parameter) {
return parameter.substring(parameter.indexOf(".") + 1);
}
public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) {
if (StringUtils.isBlank(theSpnamePrefix))
return theParamName;
return theSpnamePrefix + "." + theParamName;
}
public static Predicate[] toPredicateArray(List<Predicate> thePredicates) {
return thePredicates.toArray(new Predicate[0]);
}
private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
List<Predicate> lastUpdatedPredicates = new ArrayList<>();
if (theLastUpdated != null) {
if (theLastUpdated.getLowerBoundAsInstant() != null) {
ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
lastUpdatedPredicates.add(predicateLower);
}
if (theLastUpdated.getUpperBoundAsInstant() != null) {
Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
lastUpdatedPredicates.add(predicateUpper);
}
}
return lastUpdatedPredicates;
}
public static List<ResourcePersistentId> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<ResourcePersistentId> thePids) {
if (thePids.isEmpty()) {
return Collections.emptyList();
}
CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTable> from = cq.from(ResourceTable.class);
cq.select(from.get("myId").as(Long.class));
List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(thePids)));
cq.where(toPredicateArray(lastUpdatedPredicates));
TypedQuery<Long> query = theEntityManager.createQuery(cq);
return ResourcePersistentId.fromLongList(query.getResultList());
}
public static void verifySearchHasntFailedOrThrowInternalErrorException(Search theSearch) {
if (theSearch.getStatus() == SearchStatusEnum.FAILED) {
Integer status = theSearch.getFailureCode();
status = defaultIfNull(status, 500);
String message = theSearch.getFailureMessage();
throw BaseServerResponseException.newInstance(status, message);
}
}
public static void populateSearchEntity(SearchParameterMap theParams, String theResourceType, String theSearchUuid, String theQueryString, Search theSearch, RequestPartitionId theRequestPartitionId) {
theSearch.setDeleted(false);
theSearch.setUuid(theSearchUuid);
theSearch.setCreated(new Date());
theSearch.setTotalCount(null);
theSearch.setNumFound(0);
theSearch.setPreferredPageSize(theParams.getCount());
theSearch.setSearchType(theParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH);
theSearch.setLastUpdated(theParams.getLastUpdated());
theSearch.setResourceType(theResourceType);
theSearch.setStatus(SearchStatusEnum.LOADING);
theSearch.setSearchQueryString(theQueryString, theRequestPartitionId);
if (theParams.hasIncludes()) {
for (Include next : theParams.getIncludes()) {
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), false, next.isRecurse()));
}
}
for (Include next : theParams.getRevIncludes()) {
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), true, next.isRecurse()));
}
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@ -144,7 +145,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
}

View File

@ -12,7 +12,9 @@ import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.ArrayList;
@ -41,7 +43,10 @@ public class BaseSearchSvc {
protected DaoRegistry myDaoRegistry;
@Mock
protected DaoConfig myDaoConfig;
protected BeanFactory myBeanFactory;
@Spy
protected DaoConfig myDaoConfig = new DaoConfig();
protected static final FhirContext ourCtx = FhirContext.forDstu3Cached();

View File

@ -1,8 +1,10 @@
package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.config.SearchConfig;
import ca.uhn.fhir.jpa.dao.IResultIterator;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
@ -10,15 +12,20 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchContinuationTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTaskParameters;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.hamcrest.Matchers;
@ -28,8 +35,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,6 +73,7 @@ import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -75,9 +83,6 @@ import static org.mockito.Mockito.when;
public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
private static final Logger ourLog = LoggerFactory.getLogger(SearchCoordinatorSvcImplTest.class);
@InjectMocks
private SearchCoordinatorSvcImpl mySvc;
@Mock private SearchStrategyFactory mySearchStrategyFactory;
@Mock
@ -95,34 +100,46 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
private IRequestPartitionHelperSvc myPartitionHelperSvc;
@Mock
private ISynchronousSearchSvc mySynchronousSearchSvc;
@Spy
protected FhirContext myContext = FhirContext.forR4();
@Spy
private ExceptionService myExceptionSvc = new ExceptionService(myContext);
private SearchCoordinatorSvcImpl mySvc;
@AfterEach
public void after() {
System.clearProperty(SearchCoordinatorSvcImpl.UNIT_TEST_CAPTURE_STACK);
System.clearProperty(QueryParameterUtils.UNIT_TEST_CAPTURE_STACK);
super.after();
}
@BeforeEach
public void before() {
System.setProperty(SearchCoordinatorSvcImpl.UNIT_TEST_CAPTURE_STACK, "true");
System.setProperty(QueryParameterUtils.UNIT_TEST_CAPTURE_STACK, "true");
myCurrentSearch = null;
mySvc.setTransactionManagerForUnitTest(myTxManager);
mySvc.setContextForUnitTest(ourCtx);
mySvc.setSearchCacheServicesForUnitTest(mySearchCacheSvc, mySearchResultCacheSvc);
mySvc.setDaoRegistryForUnitTest(myDaoRegistry);
mySvc.setInterceptorBroadcasterForUnitTest(myInterceptorBroadcaster);
mySvc.setSearchBuilderFactoryForUnitTest(mySearchBuilderFactory);
mySvc.setPersistedJpaBundleProviderFactoryForUnitTest(myPersistedJpaBundleProviderFactory);
mySvc.setRequestPartitionHelperService(myPartitionHelperSvc);
mySvc.setSynchronousSearchSvc(mySynchronousSearchSvc);
DaoConfig daoConfig = new DaoConfig();
mySvc.setDaoConfigForUnitTest(daoConfig);
// Mockito has problems wiring up all
// the dependencies; particularly those in extended
// classes. This forces them in
mySvc = new SearchCoordinatorSvcImpl(
myContext,
myDaoConfig,
myInterceptorBroadcaster,
myTxManager,
mySearchCacheSvc,
mySearchResultCacheSvc,
myDaoRegistry,
mySearchBuilderFactory,
mySynchronousSearchSvc,
myPersistedJpaBundleProviderFactory,
myPartitionHelperSvc,
null, // search param registry
mySearchStrategyFactory,
myExceptionSvc,
myBeanFactory
);
}
@Test
@ -136,6 +153,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
List<ResourcePersistentId> pids = createPidSequence(800);
IResultIterator iter = new FailAfterNIterator(new SlowIterator(pids.iterator(), 2), 300);
when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
mockSearchTask();
try {
mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null, RequestPartitionId.allPartitions());
@ -161,7 +179,6 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
return null;
}).when(mySearchResultCacheSvc).storeResults(any(), anyList(), anyList());
SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME"));
@ -176,6 +193,8 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
return search;
});
mockSearchTask();
// Do all the stubbing before starting any work, since we want to avoid threading issues
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null, RequestPartitionId.allPartitions());
@ -253,6 +272,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
List<ResourcePersistentId> pids = createPidSequence(800);
SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
mockSearchTask();
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
@ -276,10 +296,10 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
}
private void initAsyncSearches() {
when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchCoordinatorSvcImpl.SearchTask.class), nullable(ISearchBuilder.class))).thenAnswer(t->{
when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchTask.class), nullable(ISearchBuilder.class))).thenAnswer(t->{
RequestDetails requestDetails = t.getArgument(0, RequestDetails.class);
Search search = t.getArgument(1, Search.class);
SearchCoordinatorSvcImpl.SearchTask searchTask = t.getArgument(2, SearchCoordinatorSvcImpl.SearchTask.class);
SearchTask searchTask = t.getArgument(2, SearchTask.class);
ISearchBuilder searchBuilder = t.getArgument(3, ISearchBuilder.class);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, searchTask, searchBuilder, requestDetails);
retVal.setDaoConfigForUnitTest(new DaoConfig());
@ -299,6 +319,9 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
List<ResourcePersistentId> pids = createPidSequence(800);
SlowIterator iter = new SlowIterator(pids.iterator(), 500);
when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
mockSearchTask();
when(myInterceptorBroadcaster.callHooks(any(), any()))
.thenReturn(true);
ourLog.info("Registering the first search");
new Thread(() -> mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null, RequestPartitionId.allPartitions())).start();
@ -355,6 +378,8 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
});
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
mockSearchTask();
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null, RequestPartitionId.allPartitions());
assertNotNull(result.getUuid());
assertEquals(790, result.size());
@ -388,6 +413,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
List<ResourcePersistentId> pids = createPidSequence(100);
SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter);
mockSearchTask();
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
@ -570,6 +596,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
search.setStatus(SearchStatusEnum.LOADING);
return Optional.of(search);
});
mockSearchTask();
when(mySearchResultCacheSvc.fetchAllResultPids(any())).thenReturn(null);
@ -711,4 +738,46 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc{
}
}
private void mockSearchTask() {
IPagingProvider pagingProvider = mock(IPagingProvider.class);
lenient().when(pagingProvider.getMaximumPageSize())
.thenReturn(500);
when(myBeanFactory.getBean(anyString(), any(SearchTaskParameters.class)))
.thenAnswer(invocation -> {
String type = invocation.getArgument(0);
switch (type) {
case SearchConfig.SEARCH_TASK:
return new SearchTask(
invocation.getArgument(1),
myTxManager,
ourCtx,
mySearchStrategyFactory,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myDaoConfig,
mySearchCacheSvc,
pagingProvider
);
case SearchConfig.CONTINUE_TASK:
return new SearchContinuationTask(
invocation.getArgument(1),
myTxManager,
ourCtx,
mySearchStrategyFactory,
myInterceptorBroadcaster,
mySearchBuilderFactory,
mySearchResultCacheSvc,
myDaoConfig,
mySearchCacheSvc,
pagingProvider,
myExceptionSvc
);
default:
fail("Invalid bean type: " + type);
return null;
}
});
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
@ -192,7 +193,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.parser.StrictErrorHandler;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Patient;
@ -28,7 +29,7 @@ public class PagingMultinodeProviderDstu3Test extends BaseResourceProviderDstu3T
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
}

View File

@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.SummaryEnum;
@ -92,7 +93,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
public void before() {
mySearchCoordinatorSvcImpl = (SearchCoordinatorSvcImpl) AopProxyUtils.getSingletonTarget(mySearchCoordinatorSvc);
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
myCaptureQueriesListener.setCaptureQueryStackTrace(true);
myDaoConfig.setAdvancedHSearchIndexing(false);
}
@ -100,7 +101,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@AfterEach
public final void after() {
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myCaptureQueriesListener.setCaptureQueryStackTrace(false);
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());

View File

@ -17,10 +17,10 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
@ -4179,7 +4179,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
runInTransaction(() -> {
ca.uhn.fhir.jpa.entity.Search search = new ca.uhn.fhir.jpa.entity.Search();
SearchCoordinatorSvcImpl.populateSearchEntity(map, "Encounter", uuid, normalized, search, RequestPartitionId.allPartitions());
QueryParameterUtils.populateSearchEntity(map, "Encounter", uuid, normalized, search, RequestPartitionId.allPartitions());
search.setStatus(SearchStatusEnum.FAILED);
search.setFailureCode(500);
search.setFailureMessage("FOO");

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.term.ZipCollectionBuilder;
import ca.uhn.fhir.jpa.test.config.TestR4Config;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
@ -28,6 +29,8 @@ import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.IHttpRequest;
import ca.uhn.fhir.rest.client.api.IHttpResponse;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.rest.gclient.ICriterion;
import ca.uhn.fhir.rest.gclient.NumberClientParam;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.NumberParam;
@ -71,6 +74,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Attachment;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.BaseResource;
import org.hl7.fhir.r4.model.Basic;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
@ -128,6 +132,7 @@ import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.StringType;
@ -142,8 +147,11 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.AopTestUtils;
@ -162,6 +170,7 @@ import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
@ -169,6 +178,7 @@ import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ca.uhn.fhir.jpa.config.r4.FhirContextR4Config.DEFAULT_PRESERVE_VERSION_REFS;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick;
@ -227,7 +237,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
mySearchCoordinatorSvcRaw.cancelAllActiveSearches();
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
@ -7144,4 +7154,315 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
return oId;
}
@Nested
public class MissingSearchParameterTests {
private interface XtoY<X, Y> {
Y doTask(X theInput);
}
private static class MissingSearchTestParameters {
/**
* The setting for IndexMissingFields
*/
public final DaoConfig.IndexEnabledEnum myEnableMissingFieldsValue;
/**
* Whether to use :missing=true/false
*/
public final boolean myIsMissing;
/**
* Whether or not the field is populated or not.
* True -> populate field.
* False -> not populated
*/
public final boolean myIsValuePresentOnResource;
public MissingSearchTestParameters(
DaoConfig.IndexEnabledEnum theEnableMissingFields,
boolean theIsMissing,
boolean theHasField
) {
myEnableMissingFieldsValue = theEnableMissingFields;
myIsMissing = theIsMissing;
myIsValuePresentOnResource = theHasField;
}
}
private IParser myParser;
@BeforeEach
public void init() {
myParser = myFhirContext.newJsonParser();
myParser.setPrettyPrint(true);
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
}
/**
* Verifies that the returned Bundle contains the resource
* with the id provided.
* @param theBundle - returned bundle
* @param theType - provided resource id
*/
private void verifyFoundBundle(Bundle theBundle, IIdType theType) {
ourLog.info(myParser.encodeResourceToString(theBundle));
assertEquals(1, theBundle.getTotal());
List<IIdType> list = toUnqualifiedVersionlessIds(theBundle);
ourLog.info(list.size() + " resources found");
IIdType type = list.get(0);
assertEquals(theType.toString(), type.toString());
}
private void verifyBundleIsEmpty(Bundle theBundle) {
ourLog.info(myParser.encodeResourceToString(theBundle));
assertEquals(0, theBundle.getTotal());
}
private IIdType createResource(Resource theResource) {
IIdType id = myClient.create()
.resource(theResource)
.prettyPrint()
.encodedXml()
.execute()
.getId()
.toUnqualifiedVersionless();
theResource.setId(id);
ourLog.info("Created:\n{}",
myParser.encodeResourceToString(theResource));
return id;
}
/**
* Runs the search on the given resource type with the given (missing) criteria
* @param theResourceClass - the resource type class
* @param theCriteria - the missing critia to use
* @return - the found bundle
*/
private Bundle doSearch(Class<? extends BaseResource> theResourceClass, ICriterion<?> theCriteria) {
//@formatter:off
return myClient
.search()
.forResource(theResourceClass)
.where(theCriteria)
.count(100)
.prettyPrint()
.returnBundle(Bundle.class)
.execute();
//@formatter:on
}
/**
* The method that generates parameters for tests
*/
private static Stream<Arguments> provideParameters() {
return Stream.of(
// 1
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.ENABLED, true, true)),
// 2
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.ENABLED, false, false)),
// 3
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.ENABLED, false, true)),
// 4
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.ENABLED, true, false)),
// 5
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.DISABLED, true, true)),
// 6
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.DISABLED, false, true)),
// 7
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.DISABLED, true, false)),
// 8
Arguments.of(new MissingSearchTestParameters(DaoConfig.IndexEnabledEnum.DISABLED, false, false))
);
}
/**
* Runs the actual test for whichever search parameter and given inputs we want.
*/
private void runTest(
MissingSearchTestParameters theParams,
XtoY<Boolean, Resource> theResourceProvider,
XtoY<Boolean, Bundle> theRunner
) {
String testMethod = new Exception().getStackTrace()[1].getMethodName();
ourLog.info(
"\nStarting {}.\nMissing fields indexed: {},\nHas Field Present: {},\nReturn resources with Missing Field: {}.\nWe expect {} returned result(s).",
testMethod,
theParams.myEnableMissingFieldsValue.name(),
theParams.myIsValuePresentOnResource,
theParams.myIsMissing,
theParams.myIsValuePresentOnResource == theParams.myIsMissing ? "0" : "1"
);
// setup
myDaoConfig.setIndexMissingFields(theParams.myEnableMissingFieldsValue);
// create our resource
Resource resource = theResourceProvider.doTask(theParams.myIsValuePresentOnResource);
// save the resource
IIdType resourceId = createResource(resource);
// run test
Bundle found = theRunner.doTask(theParams.myIsMissing);
if ((theParams.myIsMissing && !theParams.myIsValuePresentOnResource)
|| (theParams.myIsValuePresentOnResource && !theParams.myIsMissing)) {
verifyFoundBundle(found, resourceId);
} else {
verifyBundleIsEmpty(found);
}
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingStringParameter(MissingSearchTestParameters theParams) {
runTest(
theParams, (hasField) -> {
Organization org = new Organization();
if (hasField) {
org.setName("anything");
}
return org;
}, (isMissing) -> {
return doSearch(Organization.class, Organization.NAME.isMissing(isMissing));
}
);
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingDateParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
(hasField) -> {
Patient patient = new Patient();
if (hasField) {
patient.setBirthDate(new Date(2000, Calendar.DECEMBER, 25));
}
return patient;
}, (isMissing) -> {
return doSearch(Patient.class, Patient.BIRTHDATE.isMissing(isMissing));
});
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingTokenClientParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
hasField -> {
Patient patient = new Patient();
if (hasField) {
patient.setGender(AdministrativeGender.FEMALE);
}
return patient;
}, isMissing -> {
return doSearch(Patient.class, Patient.GENDER.isMissing(isMissing));
});
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingReferenceClientParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
hasField -> {
Patient patient = new Patient();
if (hasField) {
Practitioner practitioner = new Practitioner();
IIdType practitionerId = createResource(practitioner);
patient.setGeneralPractitioner(Collections.singletonList(new Reference(practitionerId)));
}
return patient;
}, isMissing -> {
return doSearch(Patient.class, Patient.GENERAL_PRACTITIONER.isMissing(isMissing));
});
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingReferenceClientParameterOnIndexedContainedResources(MissingSearchTestParameters theParams) {
myModelConfig.setIndexOnContainedResources(true);
runTest(theParams,
hasField -> {
Observation obs = new Observation();
if (hasField) {
Encounter enc = new Encounter();
IIdType id = createResource(enc);
obs.setEncounter(new Reference(id));
}
return obs;
}, isMissing -> {
ICriterion<?> criterion = Observation.ENCOUNTER.isMissing(isMissing);
return doSearch(Observation.class, criterion);
});
myModelConfig.setIndexOnContainedResources(false);
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingURLParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
hasField -> {
String methodName = new Exception().getStackTrace()[0].getMethodName();
SearchParameter sp = new SearchParameter();
sp.addBase("MolecularSequence");
sp.setCode(methodName);
sp.setType(Enumerations.SearchParamType.NUMBER);
sp.setExpression("MolecularSequence.variant-end");
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
if (hasField) {
sp.setUrl("http://example.com");
}
return sp;
}, isMissing -> {
return doSearch(SearchParameter.class, SearchParameter.URL.isMissing(isMissing));
});
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingQuantityClientParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
hasField -> {
Observation obs = new Observation();
if (hasField) {
obs.setValue(new Quantity(3));
}
return obs;
}, isMissing -> {
return doSearch(Observation.class, Observation.VALUE_QUANTITY.isMissing(isMissing));
});
}
@ParameterizedTest
@MethodSource("provideParameters")
public void testMissingNumberClientParameter(MissingSearchTestParameters theParams) {
runTest(theParams,
hasField -> {
String methodName = new Exception().getStackTrace()[0].getMethodName();
MolecularSequence molecularSequence = new MolecularSequence();
if (hasField) {
molecularSequence.setVariant(Collections.singletonList(
new MolecularSequence.MolecularSequenceVariantComponent().setEnd(1)
));
}
return molecularSequence;
}, isMissing -> {
NumberClientParam numberClientParam = new NumberClientParam("variant-end");
return doSearch(
MolecularSequence.class,
numberClientParam.isMissing(isMissing)
);
});
}
}
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.gclient.StringClientParam;
@ -28,7 +29,7 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
super.after();
myDaoConfig.setCountSearchResultsUpTo(null);
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myDaoConfig.setDefaultTotalMode(null);
}

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.search.r4;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.parser.StrictErrorHandler;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
@ -28,7 +29,7 @@ public class PagingMultinodeProviderR4Test extends BaseResourceProviderR4Test {
myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences());
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcRaw.setNeverUseLocalSearchForUnitTests(false);
}