In-memory matcher (#1116)

* Initial refactoring to move database matcher out into its own class

* MAJOR REFACTOR: Pulled indexing code out of BaseHapiFhirDao into a new class ResourceIndexedSearchParams

* Moved calculateHashes

* Replaced @Bean definitions in BaseConfig.java with @ComponentScan

Annotated bean classes with either @Service (if it's stateless) or @Component (if it's stateful).  It doesn't really matter which annotation is used, but it's helpful to see at a glance whether a bean is stateful or stateless.

* Move services out of BaseHapiFhirDao

Moved services required by ResourceIndexedSearchParams out of BaseHapiFhirDao and into new classes called LogicalReferenceHelper, IdHelperService, MatchUrlService, and DaoProvider.

Converted SearchBuilder into Prototype Bean

Mark Spring components that depend on daos and entitymanagers with @Lazy so they aren't picked up by hapi-fhir-spring-boot-autoconfigure.

* Added SubscriptionMatcherInMemory

Moved static data out of BaseHapiFhirDao into ResourceMetaParams

Moved translateMatchUrl methods out of BaseHapiFhirDao into MatchUrlService bean

Simplified SubscriptionMatcherInMemory to not depend on entity or dao

Turned all subscribers into prototype beans

* Moved searchParam method out to mySearchParamProvider

Also removed dao and contest parameters from of myMatchUrlService methods

Moved code out of SearchBuilder into SearchParameterMap.clean() so it can be used by inMemoryMatcher

Introduced a new composite subscription matcher that tries to match in memory and if it finds a parameter in the criteria it doesn't support, it falls back to the database matcher.

* Added support for references

Also fixed a small bug in SearchParameterMap that was missing the ";" after "_has" when creating a normalized query from search params.

* Finished implementing all tests from FhirResourceDaoR4SearchNoFtTest

* Make in-memory matcher configurable, disabled by default

* Validate Subscription criteria when they're submitted

Send HTTP 422 UnprocessableEntityException if the criteria fail validation.

* fixed Sonar "Blocker" issues.

*  Don't reload the resource before sending it out

Since we can always force a reload using restHookDetails.isDeliverLatestVersion

* Added tests to cover Custom Search param.

* Split ResourceIndexedSearchParam into separate state and service classes

* Cleaned up SearchBuilder.

Removed uses of myCallingDao as an injection mechanism.
Left // FIXME KHS cookie crumbs to clean up

* Reduced dependencies on BaseHapiFhirDao

Removed methods from IDao interface that were used for injection

* Updated change log
This commit is contained in:
Ken Stevens 2018-11-19 16:19:52 -05:00 committed by GitHub
parent 45a5db6fd8
commit 03ebcafdf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 3703 additions and 1759 deletions

View File

@ -60,7 +60,7 @@ public interface IQueryParameterType extends Serializable {
public String getValueAsQueryToken(FhirContext theContext);
/**
* This method will return any qualifier that should be appended to the parameter name (e.g ":exact")
* This method will return any qualifier that should be appended to the parameter name (e.g ":exact"). Returns null if none are present.
*/
public String getQueryParameterQualifier();

View File

@ -100,7 +100,11 @@ public class TokenParam extends BaseParam /*implements IQueryParameterType*/ {
@Override
String doGetValueAsQueryToken(FhirContext theContext) {
if (getSystem() != null) {
if (getValue() != null) {
return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|' + ParameterUtil.escape(getValue());
} else {
return ParameterUtil.escape(StringUtils.defaultString(getSystem())) + '|';
}
}
return ParameterUtil.escape(getValue());
}

View File

@ -670,7 +670,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<argLine>@{argLine} -Dfile.encoding=UTF-8 -Xmx1024m</argLine>
<argLine>@{argLine} -Dfile.encoding=UTF-8 -Xmx20484M -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=2048M -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC</argLine>
<forkCount>0.6C</forkCount>
</configuration>
</plugin>

View File

@ -1,5 +1,30 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import javax.annotation.Nonnull;
/*
* #%L
* HAPI FHIR JPA Server
@ -20,46 +45,14 @@ package ca.uhn.fhir.jpa.config;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.*;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.hibernate5.HibernateExceptionTranslator;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import javax.annotation.Nonnull;
@Configuration
@EnableScheduling
@EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data")
@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters={
@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=BaseConfig.class),
@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=WebSocketConfigurer.class)})
public abstract class BaseConfig implements SchedulingConfigurer {
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
@ -67,11 +60,6 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Autowired
protected Environment myEnv;
@Bean(name = "myDaoRegistry")
public DaoRegistry daoRegistry() {
return new DaoRegistry();
}
@Override
public void configureTasks(@Nonnull ScheduledTaskRegistrar theTaskRegistrar) {
theTaskRegistrar.setTaskScheduler(taskScheduler());
@ -95,21 +83,6 @@ public abstract class BaseConfig implements SchedulingConfigurer {
public abstract FhirContext fhirContext();
@Bean
public ICacheWarmingSvc cacheWarmingSvc() {
return new CacheWarmingSvcImpl();
}
@Bean
public HibernateExceptionTranslator hibernateExceptionTranslator() {
return new HibernateExceptionTranslator();
}
@Bean
public HibernateJpaDialect hibernateJpaDialectInstance() {
return new HibernateJpaDialect();
}
@Bean()
public ScheduledExecutorFactoryBean scheduledExecutorService() {
ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean();
@ -124,44 +97,6 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new SubscriptionTriggeringProvider();
}
@Bean(autowire = Autowire.BY_TYPE, name = "mySearchCoordinatorSvc")
public ISearchCoordinatorSvc searchCoordinatorSvc() {
return new SearchCoordinatorSvcImpl();
}
@Bean
public ISearchParamPresenceSvc searchParamPresenceSvc() {
return new SearchParamPresenceSvcImpl();
}
@Bean(autowire = Autowire.BY_TYPE)
public IStaleSearchDeletingSvc staleSearchDeletingSvc() {
return new StaleSearchDeletingSvcImpl();
}
/**
* Note: If you're going to use this, you need to provide a bean
* of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender}
* in your own Spring config
*/
@Bean
@Lazy
public SubscriptionEmailInterceptor subscriptionEmailInterceptor() {
return new SubscriptionEmailInterceptor();
}
@Bean
@Lazy
public SubscriptionRestHookInterceptor subscriptionRestHookInterceptor() {
return new SubscriptionRestHookInterceptor();
}
@Bean
@Lazy
public SubscriptionWebsocketInterceptor subscriptionWebsocketInterceptor() {
return new SubscriptionWebsocketInterceptor();
}
@Bean()
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
@ -192,13 +127,4 @@ public abstract class BaseConfig implements SchedulingConfigurer {
private static HibernateJpaDialect hibernateJpaDialect(HapiLocalizer theLocalizer) {
return new HapiFhirHibernateJpaDialect(theLocalizer);
}
/**
* This lets the "@Value" fields reference properties from the properties file
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}

View File

@ -1,127 +1,11 @@
package ca.uhn.fhir.jpa.dao;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import java.io.CharArrayWriter;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.*;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IDomainResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.BaseResource;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.index.IndexingSupport;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.BaseTag;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.entity.ResourceHistoryTag;
import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
import ca.uhn.fhir.jpa.entity.SearchResult;
import ca.uhn.fhir.jpa.entity.SearchStatusEnum;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.entity.SubscriptionTable;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.entity.TagTypeEnum;
import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptMap;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
import ca.uhn.fhir.jpa.dao.index.SearchParamExtractorService;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
@ -130,8 +14,6 @@ import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
@ -146,25 +28,11 @@ import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.LenientErrorHandler;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.UriAndListParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
@ -173,6 +41,45 @@ import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.XmlUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.Validate;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.*;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent;
import java.io.CharArrayWriter;
import java.text.Normalizer;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.commons.lang3.StringUtils.*;
/*
* #%L
@ -196,7 +103,7 @@ import ca.uhn.fhir.util.XmlUtil;
@SuppressWarnings("WeakerAccess")
@Repository
public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware, IndexingSupport {
public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ApplicationContextAware {
public static final long INDEX_STATUS_INDEXED = 1L;
public static final long INDEX_STATUS_INDEXING_FAILED = 2L;
@ -205,46 +112,17 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
public static final String OO_SEVERITY_INFO = "information";
public static final String OO_SEVERITY_WARN = "warning";
public static final String UCUM_NS = "http://unitsofmeasure.org";
static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
static final Map<String, Class<? extends IQueryParameterAnd<?>>> RESOURCE_META_AND_PARAMS;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class);
private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>();
private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
private static boolean ourValidationDisabledForUnitTest;
private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
Map<String, Class<? extends IQueryParameterAnd<?>>> resourceMetaAndParams = new HashMap<String, Class<? extends IQueryParameterAnd<?>>>();
resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class);
resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class);
resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class);
resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class);
resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class);
resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<String>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
}
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
protected IdHelperService myIdHelperService;
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired
protected ISearchResultDao mySearchResultDao;
@ -294,27 +172,34 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
@Autowired
private ISearchParamPresenceSvc mySearchParamPresenceSvc;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
protected ISearchParamRegistry mySearchParamRegistry;
//@Autowired
//private ISearchResultDao mySearchResultDao;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@Autowired
private BeanFactory beanFactory;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private SearchParamExtractorService mySearchParamExtractorService;
private ApplicationContext myApplicationContext;
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
}
}
/**
* Returns the newly created forced ID. If the entity already had a forced ID, or if
* none was created, returns null.
*/
protected ForcedId createForcedIdIfNeeded(ResourceTable theEntity, IIdType theId, boolean theCreateForPureNumericIds) {
if (theId.isEmpty() == false && theId.hasIdPart() && theEntity.getForcedId() == null) {
if (!theCreateForPureNumericIds && isValidPid(theId)) {
if (!theCreateForPureNumericIds && IdHelperService.isValidPid(theId)) {
return null;
}
@ -538,7 +423,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
ForcedId forcedId = resource.getForcedId();
resource.setForcedId(null);
myResourceTableDao.saveAndFlush(resource);
myForcedIdDao.delete(forcedId);
myIdHelperService.delete(forcedId);
}
myResourceTableDao.delete(resource);
@ -656,7 +541,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theResourceName != null) {
Predicate typePredicate = builder.equal(from.get("myResourceType"), theResourceName);
if (theResourceId != null) {
cq.where(typePredicate, builder.equal(from.get("myResourceId"), translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
cq.where(typePredicate, builder.equal(from.get("myResourceId"), myIdHelperService.translateForcedIdToPid(theResourceName, theResourceId.getIdPart())));
} else {
cq.where(typePredicate);
}
@ -688,8 +573,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
@Override
public DaoConfig getConfig() {
protected DaoConfig getConfig() {
return myConfig;
}
@ -719,51 +603,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
@Override
@SuppressWarnings("unchecked")
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> resourceTypeToDao = getDaos();
IFhirResourceDao<R> dao = (IFhirResourceDao<R>) resourceTypeToDao.get(theType);
return dao;
return myDaoRegistry.getResourceDaoIfExists(theType);
}
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getDaos() {
if (myResourceTypeToDao == null) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> resourceTypeToDao = new HashMap<>();
Map<String, IFhirResourceDao> daos = myApplicationContext.getBeansOfType(IFhirResourceDao.class, false, false);
String[] beanNames = myApplicationContext.getBeanNamesForType(IFhirResourceDao.class);
for (IFhirResourceDao<?> next : daos.values()) {
resourceTypeToDao.put(next.getResourceType(), next);
}
if (this instanceof IFhirResourceDao<?>) {
IFhirResourceDao<?> thiz = (IFhirResourceDao<?>) this;
resourceTypeToDao.put(thiz.getResourceType(), thiz);
}
myResourceTypeToDao = resourceTypeToDao;
}
return Collections.unmodifiableMap(myResourceTypeToDao);
}
@Override
public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao() {
return myResourceIndexedCompositeStringUniqueDao;
}
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
Map<String, RuntimeSearchParam> params = mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName());
return params.get(theParamName);
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return mySearchParamRegistry.getActiveSearchParams(theResourceDef.getName()).values();
protected IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
return myDaoRegistry.getDaoOrThrowException(theClass);
}
protected TagDefinition getTagOrNull(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -901,25 +747,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theProvider.setSearchCoordinatorSvc(mySearchCoordinatorSvc);
}
@Override
public boolean isLogicalReference(IIdType theId) {
Set<String> treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical();
if (treatReferencesAsLogical != null) {
for (String nextLogicalRef : treatReferencesAsLogical) {
nextLogicalRef = trim(nextLogicalRef);
if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') {
if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) {
return true;
}
} else {
if (theId.getValue().equals(nextLogicalRef)) {
return true;
}
}
}
}
return false;
return LogicalReferenceHelper.isLogicalReference(myConfig, theId);
}
public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
@ -930,10 +759,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
@Override
public SearchBuilder newSearchBuilder() {
SearchBuilder builder = new SearchBuilder(
getContext(), myEntityManager, myFulltextSearchSvc, this, myResourceIndexedSearchParamUriDao,
myForcedIdDao, myTerminologySvc, mySerarchParamRegistry, myResourceTagDao, myResourceViewDao);
return builder;
return beanFactory.getBean(SearchBuilder.class, this);
}
public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
@ -970,7 +796,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal.toString();
}
@Override
public void populateFullTextFields(final IBaseResource theResource, ResourceTable theEntity) {
if (theEntity.getDeleted() != null) {
theEntity.setNarrativeTextParsedIntoWords(null);
@ -1015,7 +840,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (theEntity.getDeleted() == null) {
encoding = myConfig.getResourceEncoding();
Set<String> excludeElements = EXCLUDE_ELEMENTS_IN_ENCODED;
Set<String> excludeElements = ResourceMetaParams.EXCLUDE_ELEMENTS_IN_ENCODED;
theEntity.setFhirVersion(myContext.getVersion().getVersion());
bytes = encodeResource(theResource, encoding, excludeElements, myContext);
@ -1256,25 +1081,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
// nothing
}
@Override
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theResourceType);
SearchParameterMap paramMap = translateMatchUrl(this, myContext, theMatchUrl, resourceDef);
paramMap.setLoadSynchronous(true);
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
}
IFhirResourceDao<R> dao = getDao(theResourceType);
if (dao == null) {
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
}
return dao.searchForIds(paramMap);
}
@CoverageIgnore
public BaseHasResource readEntity(IIdType theValueId) {
throw new NotImplementedException("");
@ -1343,11 +1149,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return false;
}
@PostConstruct
public void startClearCaches() {
myResourceTypeToDao = null;
}
private ExpungeOutcome toExpungeOutcome(ExpungeOptions theExpungeOptions, AtomicInteger theRemainingCount) {
return new ExpungeOutcome()
.setDeletedCount(theExpungeOptions.getLimit() - theRemainingCount.get());
@ -1461,7 +1262,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
@Override
public String toResourceName(Class<? extends IBaseResource> theResourceType) {
return myContext.getResourceDefinition(theResourceType).getName();
}
@ -1475,16 +1275,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return new SliceImpl<>(Collections.singletonList(theVersion.getId()));
}
@Override
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
return translateForcedIdToPids(getConfig(), new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
}
protected List<Long> translateForcedIdToPids(IIdType theId) {
return translateForcedIdToPids(getConfig(), theId, myForcedIdDao);
}
@SuppressWarnings("unchecked")
protected ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, ResourceTable
theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
@ -1516,14 +1306,14 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theEntity.setPublished(theUpdateTime);
}
ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(this, theEntity);
ResourceIndexedSearchParams existingParams = new ResourceIndexedSearchParams(theEntity);
ResourceIndexedSearchParams newParams = null;
EncodedResource changed;
if (theDeletedTimestampOrNull != null) {
newParams = new ResourceIndexedSearchParams(this);
newParams = new ResourceIndexedSearchParams();
theEntity.setDeleted(theDeletedTimestampOrNull);
theEntity.setUpdated(theDeletedTimestampOrNull);
@ -1539,7 +1329,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
if (thePerformIndexing) {
newParams = new ResourceIndexedSearchParams(this, theUpdateTime, theEntity, theResource, existingParams);
newParams = new ResourceIndexedSearchParams();
mySearchParamExtractorService.populateFromResource(newParams, this, theUpdateTime, theEntity, theResource, existingParams);
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
@ -1550,7 +1341,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
theEntity.setLanguage(((IAnyResource) theResource).getLanguageElement().getValue());
}
newParams.setParams(theEntity);
newParams.setParamsOn(theEntity);
theEntity.setIndexStatus(INDEX_STATUS_INDEXED);
populateFullTextFields(theResource, theEntity);
} else {
@ -1625,7 +1416,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/
if (thePerformIndexing) {
Map<String, Boolean> presentSearchParams = new HashMap<>();
// TODO KHS null check?
for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
presentSearchParams.put(nextKey, Boolean.TRUE);
}
@ -1644,8 +1434,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
* Indexing
*/
if (thePerformIndexing) {
newParams.removeCommon(theEntity, existingParams);
mySearchParamExtractorService.removeCommon(newParams, theEntity, existingParams);
} // if thePerformIndexing
if (theResource != null) {
@ -1884,44 +1673,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return bytes;
}
protected static boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);
if (nextChar < '0' || nextChar > '9') {
return false;
}
}
return true;
}
@CoverageIgnore
protected static IQueryParameterAnd<?> newInstanceAnd(String chain) {
IQueryParameterAnd<?> type;
Class<? extends IQueryParameterAnd<?>> clazz = RESOURCE_META_AND_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
@CoverageIgnore
protected static IQueryParameterType newInstanceType(String chain) {
IQueryParameterType type;
Class<? extends IQueryParameterType> clazz = RESOURCE_META_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
public static String normalizeString(String theString) {
CharArrayWriter outBuffer = new CharArrayWriter(theString.length());
@ -2004,140 +1755,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return retVal;
}
protected static Long translateForcedIdToPid(DaoConfig theDaoConfig, String theResourceName, String theResourceId, IForcedIdDao
theForcedIdDao) {
return translateForcedIdToPids(theDaoConfig, new IdDt(theResourceName, theResourceId), theForcedIdDao).get(0);
}
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
Validate.isTrue(theId.hasIdPart());
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
return Collections.singletonList(theId.getIdPartAsLong());
} else {
List<ForcedId> forcedId;
if (theId.hasResourceType()) {
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
} else {
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
}
if (forcedId.isEmpty() == false) {
List<Long> retVal = new ArrayList<>(forcedId.size());
for (ForcedId next : forcedId) {
retVal.add(next.getResourcePid());
}
return retVal;
} else {
throw new ResourceNotFoundException(theId);
}
}
}
public static SearchParameterMap translateMatchUrl(IDao theCallingDao, FhirContext theContext, String
theMatchUrl, RuntimeResourceDefinition resourceDef) {
SearchParameterMap paramMap = new SearchParameterMap();
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
for (NameValuePair next : parameters) {
if (isBlank(next.getValue())) {
continue;
}
String paramName = next.getName();
String qualifier = null;
for (int i = 0; i < paramName.length(); i++) {
switch (paramName.charAt(i)) {
case '.':
case ':':
qualifier = paramName.substring(i);
paramName = paramName.substring(0, i);
i = Integer.MAX_VALUE - 1;
break;
}
}
QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
nameToParamLists.put(paramName, paramList);
}
for (String nextParamName : nameToParamLists.keySet()) {
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
if (paramList != null && paramList.size() > 0) {
if (paramList.size() > 2) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions");
} else {
DateRangeParam p1 = new DateRangeParam();
p1.setValuesAsQueryTokens(theContext, nextParamName, paramList);
paramMap.setLastUpdated(p1);
}
}
continue;
}
if (Constants.PARAM_HAS.equals(nextParamName)) {
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
paramMap.add(nextParamName, param);
continue;
}
if (Constants.PARAM_COUNT.equals(nextParamName)) {
if (paramList.size() > 0 && paramList.get(0).size() > 0) {
String intString = paramList.get(0).get(0);
try {
paramMap.setCount(Integer.parseInt(intString));
} catch (NumberFormatException e) {
throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString);
}
}
continue;
}
if (RESOURCE_META_PARAMS.containsKey(nextParamName)) {
if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) {
throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier());
}
IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
type.setValuesAsQueryTokens(theContext, nextParamName, (paramList));
paramMap.add(nextParamName, type);
} else if (nextParamName.startsWith("_")) {
// ignore these since they aren't search params (e.g. _sort)
} else {
RuntimeSearchParam paramDef = theCallingDao.getSearchParamByName(resourceDef, nextParamName);
if (paramDef == null) {
throw new InvalidRequestException(
"Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName);
}
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(theContext, paramDef, nextParamName, paramList);
paramMap.add(nextParamName, param);
}
}
return paramMap;
}
public static List<NameValuePair> translateMatchUrl(String theMatchUrl) {
List<NameValuePair> parameters;
String matchUrl = theMatchUrl;
int questionMarkIndex = matchUrl.indexOf('?');
if (questionMarkIndex != -1) {
matchUrl = matchUrl.substring(questionMarkIndex + 1);
}
matchUrl = matchUrl.replace("|", "%7C");
matchUrl = matchUrl.replace("=>=", "=%3E%3D");
matchUrl = matchUrl.replace("=<=", "=%3C%3D");
matchUrl = matchUrl.replace("=>", "=%3E");
matchUrl = matchUrl.replace("=<", "=%3C");
if (matchUrl.contains(" ")) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)");
}
parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&');
return parameters;
}
public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
if (!theResourceName.equals(theEntity.getResourceType())) {
throw new ResourceNotFoundException(
@ -2145,28 +1762,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
}
@Override
public ISearchParamExtractor getSearchParamExtractor() {
return mySearchParamExtractor;
}
@Override
public ISearchParamRegistry getSearchParamRegistry() {
return mySearchParamRegistry;
}
@Override
public EntityManager getEntityManager() {
return myEntityManager;
}
@Override
public Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getResourceTypeToDao() {
return myResourceTypeToDao;
}
@Override
public IForcedIdDao getForcedIdDao() {
return myForcedIdDao;
}
}

View File

@ -25,7 +25,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
@ -75,8 +74,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
@Autowired()
protected ISearchResultDao mySearchResultDao;
@Autowired
protected DaoConfig myDaoConfig;
@Autowired
@ -86,6 +83,8 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
private String mySecondaryPrimaryKeyParamName;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@ -272,7 +271,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public DeleteMethodOutcome deleteByUrl(String theUrl, List<DeleteConflict> deleteConflicts, RequestDetails theRequest) {
StopWatch w = new StopWatch();
Set<Long> resource = processMatchUrl(theUrl, myResourceType);
Set<Long> resource = myMatchUrlService.processMatchUrl(theUrl, myResourceType);
if (resource.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) {
throw new PreconditionFailedException(getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
@ -372,7 +371,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
entity.setResourceType(toResourceName(theResource));
if (isNotBlank(theIfNoneExist)) {
Set<Long> match = processMatchUrl(theIfNoneExist, myResourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(theIfNoneExist, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
throw new PreconditionFailedException(msg);
@ -793,7 +792,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
myResourceName = def.getName();
if (mySecondaryPrimaryKeyParamName != null) {
RuntimeSearchParam sp = getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
RuntimeSearchParam sp = mySearchParamRegistry.getSearchParamByName(def, mySecondaryPrimaryKeyParamName);
if (sp == null) {
throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]");
}
@ -849,7 +848,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Override
public Set<Long> processMatchUrl(String theMatchUrl) {
return processMatchUrl(theMatchUrl, getResourceType());
return myMatchUrlService.processMatchUrl(theMatchUrl, getResourceType());
}
@Override
@ -911,7 +910,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
Long pid = translateForcedIdToPid(getResourceName(), theId.getIdPart());
Long pid = myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart());
BaseHasResource entity = myEntityManager.find(ResourceTable.class, pid);
if (entity == null) {
@ -951,7 +950,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
protected ResourceTable readEntityLatestVersion(IIdType theId) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, translateForcedIdToPid(getResourceName(), theId.getIdPart()));
ResourceTable entity = myEntityManager.find(ResourceTable.class, myIdHelperService.translateForcedIdToPid(getResourceName(), theId.getIdPart()));
if (entity == null) {
throw new ResourceNotFoundException(theId);
}
@ -1192,7 +1191,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Should not be null since the check above would have caught it
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceName);
RuntimeSearchParam paramDef = getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, qualifiedParamName.getParamName());
for (String nextValue : theSource.get(nextParamName)) {
if (isNotBlank(nextValue)) {
@ -1232,7 +1231,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
IIdType resourceId;
if (isNotBlank(theMatchUrl)) {
StopWatch sw = new StopWatch();
Set<Long> match = processMatchUrl(theMatchUrl, myResourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(theMatchUrl, myResourceType);
if (match.size() > 1) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
throw new PreconditionFailedException(msg);

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
import ca.uhn.fhir.jpa.util.ExpungeOutcome;
@ -51,8 +50,6 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
@Autowired
@Qualifier("myResourceCountsCache")
public ResourceCountCache myResourceCountsCache;
@Autowired
private IForcedIdDao myForcedIdDao;
private ReentrantLock myReindexLock = new ReentrantLock(false);
@Autowired
private ITermConceptDao myTermConceptDao;
@ -60,9 +57,6 @@ public abstract class BaseHapiFhirSystemDao<T, MT> extends BaseHapiFhirDao<IBase
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private IResourceTableDao myResourceTableDao;
@Override
@Transactional(propagation = Propagation.NEVER)

View File

@ -348,5 +348,16 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
protected abstract RuntimeSearchParam toRuntimeSp(SP theNextSp);
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
Map<String, RuntimeSearchParam> params = getActiveSearchParams(theResourceDef.getName());
return params.get(theParamName);
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return getActiveSearchParams(theResourceDef.getName()).values();
}
}

View File

@ -156,6 +156,7 @@ public class DaoConfig {
private List<Integer> mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1);
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/**
@ -1448,6 +1449,50 @@ public class DaoConfig {
myDisableHashBasedSearches = theDisableHashBasedSearches;
}
/**
* If set to <code>false</code> (default is true) the server will not use
* in-memory subscription searching and instead use the database matcher for all subscription
* criteria matching.
* <p>
* When there are subscriptions registered
* on the server, the default behaviour is to compare the changed resource to the
* subscription criteria directly in-memory without going out to the database.
* Certain types of subscription criteria, e.g. chained references of queries with
* qualifiers or prefixes, are not supported by the in-memory matcher and will fall back
* to a database matcher.
* <p>
* The database matcher performs a query against the
* database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity
*
* @since 3.6.1
*/
public boolean isEnableInMemorySubscriptionMatching() {
return myEnableInMemorySubscriptionMatching;
}
/**
* If set to <code>false</code> (default is true) the server will not use
* in-memory subscription searching and instead use the database matcher for all subscription
* criteria matching.
* <p>
* When there are subscriptions registered
* on the server, the default behaviour is to compare the changed resource to the
* subscription criteria directly in-memory without going out to the database.
* Certain types of subscription criteria, e.g. chained references of queries with
* qualifiers or prefixes, are not supported by the in-memory matcher and will fall back
* to a database matcher.
* <p>
* The database matcher performs a query against the
* database by prepending ?id=XYZ to the subscription criteria where XYZ is the id of the changed entity
*
* @since 3.6.1
*/
public void setEnableInMemorySubscriptionMatching(boolean theEnableInMemorySubscriptionMatching) {
myEnableInMemorySubscriptionMatching = theEnableInMemorySubscriptionMatching;
}
public enum IndexEnabledEnum {
ENABLED,
DISABLED

View File

@ -23,22 +23,27 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component("myDaoRegistry")
public class DaoRegistry implements ApplicationContextAware {
private ApplicationContext myAppCtx;
@Autowired
private FhirContext myCtx;
private FhirContext myContext;
private volatile Map<String, IFhirResourceDao<?>> myResourceNameToResourceDao;
private volatile IFhirSystemDao<?, ?> mySystemDao;
@ -47,8 +52,8 @@ public class DaoRegistry implements ApplicationContextAware {
myAppCtx = theApplicationContext;
}
public IFhirSystemDao<?, ?> getSystemDao() {
IFhirSystemDao<?, ?> retVal = mySystemDao;
public IFhirSystemDao getSystemDao() {
IFhirSystemDao retVal = mySystemDao;
if (retVal == null) {
retVal = myAppCtx.getBean(IFhirSystemDao.class);
mySystemDao = retVal;
@ -56,10 +61,11 @@ public class DaoRegistry implements ApplicationContextAware {
return retVal;
}
public IFhirResourceDao<?> getResourceDao(String theResourceName) {
IFhirResourceDao<?> retVal = getResourceNameToResourceDao().get(theResourceName);
public IFhirResourceDao getResourceDao(String theResourceName) {
init();
IFhirResourceDao retVal = myResourceNameToResourceDao.get(theResourceName);
if (retVal == null) {
List<String> supportedResourceTypes = getResourceNameToResourceDao()
List<String> supportedResourceTypes = myResourceNameToResourceDao
.keySet()
.stream()
.sorted()
@ -67,26 +73,54 @@ public class DaoRegistry implements ApplicationContextAware {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + theResourceName + " - Can handle: " + supportedResourceTypes);
}
return retVal;
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDao(Class<T> theResourceType) {
String resourceName = myCtx.getResourceDefinition(theResourceType).getName();
public <R extends IBaseResource> IFhirResourceDao<R> getResourceDao(Class<R> theResourceType) {
IFhirResourceDao<R> retVal = getResourceDaoIfExists(theResourceType);
Validate.notNull(retVal, "No DAO exists for resource type %s - Have: %s", theResourceType, myResourceNameToResourceDao);
return retVal;
}
public <T extends IBaseResource> IFhirResourceDao<T> getResourceDaoIfExists(Class<T> theResourceType) {
String resourceName = myContext.getResourceDefinition(theResourceType).getName();
return (IFhirResourceDao<T>) getResourceDao(resourceName);
}
private Map<String, IFhirResourceDao<?>> getResourceNameToResourceDao() {
Map<String, IFhirResourceDao<?>> retVal = myResourceNameToResourceDao;
if (retVal == null || retVal.isEmpty()) {
retVal = new HashMap<>();
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
for (IFhirResourceDao nextResourceDao : resourceDaos.values()) {
RuntimeResourceDefinition nextResourceDef = myCtx.getResourceDefinition(nextResourceDao.getResourceType());
retVal.put(nextResourceDef.getName(), nextResourceDao);
private void init() {
if (myResourceNameToResourceDao != null && !myResourceNameToResourceDao.isEmpty()) {
return;
}
myResourceNameToResourceDao = retVal;
Map<String, IFhirResourceDao> resourceDaos = myAppCtx.getBeansOfType(IFhirResourceDao.class);
initializeMaps(resourceDaos.values());
}
private void initializeMaps(Collection<IFhirResourceDao> theResourceDaos) {
myResourceNameToResourceDao = new HashMap<>();
for (IFhirResourceDao nextResourceDao : theResourceDaos) {
RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(nextResourceDao.getResourceType());
myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao);
}
}
public IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao retVal = getResourceDao(theClass);
if (retVal == null) {
List<String> supportedResourceNames = myResourceNameToResourceDao
.keySet()
.stream()
.map(t -> myContext.getResourceDefinition(t).getName())
.sorted()
.collect(Collectors.toList());
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + myContext.getResourceDefinition(theClass).getName() + " - Can handle: " + supportedResourceNames);
}
return retVal;
}
public void setResourceDaos(Collection<IFhirResourceDao> theResourceDaos) {
initializeMaps(theResourceDaos);
}
}

View File

@ -68,7 +68,6 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.TypedQuery;
@ -82,6 +81,8 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private DaoRegistry myDaoRegistry;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
@ -243,7 +244,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
requestDetails.setParameters(new HashMap<String, String[]>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = translateMatchUrl(params);
List<NameValuePair> parameters = myMatchUrlService.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.StringParam;
@ -41,7 +42,6 @@ import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hl7.fhir.dstu3.model.BaseResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
@ -65,11 +65,14 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
@Autowired
protected IForcedIdDao myForcedIdDao;
private Boolean ourDisabled;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private IdHelperService myIdHelperService;
private Boolean ourDisabled;
/**
* Constructor
*/
@ -225,7 +228,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
StringParam idParm = (StringParam) idParam;
idParamValue = idParm.getValue();
}
pid = BaseHapiFhirDao.translateForcedIdToPid(myDaoConfig, theResourceName, idParamValue, myForcedIdDao);
pid = myIdHelperService.translateForcedIdToPid(theResourceName, idParamValue);
}
Long referencingPid = pid;
@ -278,7 +281,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (contextParts.length != 3 || "Patient".equals(contextParts[0]) == false || "$everything".equals(contextParts[2]) == false) {
throw new InvalidRequestException("Invalid context: " + theContext);
}
Long pid = BaseHapiFhirDao.translateForcedIdToPid( myDaoConfig, contextParts[0], contextParts[1], myForcedIdDao);
Long pid = myIdHelperService.translateForcedIdToPid(contextParts[0], contextParts[1]);
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);

View File

@ -1,17 +1,13 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Collection;
import java.util.Set;
/*
* #%L
@ -41,10 +37,6 @@ public interface IDao {
FhirContext getContext();
RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName);
Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef);
/**
* Populate all of the runtime dependencies that a bundle provider requires in order to work
*/
@ -52,12 +44,9 @@ public interface IDao {
ISearchBuilder newSearchBuilder();
void populateFullTextFields(IBaseResource theResource, ResourceTable theEntity);
<R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType);
IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation);
<R extends IBaseResource> R toResource(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<ResourceTag> theTagList, boolean theForHistoryOperation);
ISearchParamRegistry getSearchParamRegistry();
}

View File

@ -42,7 +42,7 @@ public interface ISearchBuilder {
void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager,
FhirContext theContext, IDao theDao);
Set<Long> loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
Set<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes, boolean theReverseMode,
DateRangeParam theLastUpdated, String theSearchIdOrDescription);
/**

View File

@ -20,9 +20,11 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -53,4 +55,8 @@ public interface ISearchParamRegistry {
* Request that the cache be refreshed at the next convenient time (in a different thread)
*/
void requestRefresh();
RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName);
Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef);
}

View File

@ -0,0 +1,32 @@
package ca.uhn.fhir.jpa.dao;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.trim;
public class LogicalReferenceHelper {
public static boolean isLogicalReference(DaoConfig myConfig, IIdType theId) {
Set<String> treatReferencesAsLogical = myConfig.getTreatReferencesAsLogical();
if (treatReferencesAsLogical != null) {
for (String nextLogicalRef : treatReferencesAsLogical) {
nextLogicalRef = trim(nextLogicalRef);
if (nextLogicalRef.charAt(nextLogicalRef.length() - 1) == '*') {
if (theId.getValue().startsWith(nextLogicalRef.substring(0, nextLogicalRef.length() - 1))) {
return true;
}
} else {
if (theId.getValue().equals(nextLogicalRef)) {
return true;
}
}
}
}
return false;
}
}

View File

@ -0,0 +1,186 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.CoverageIgnore;
import com.google.common.collect.ArrayListMultimap;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
public class MatchUrlService {
@Autowired
private FhirContext myContext;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType);
SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef);
paramMap.setLoadSynchronous(true);
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
}
IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType);
if (dao == null) {
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
}
return dao.searchForIds(paramMap);
}
public SearchParameterMap translateMatchUrl(String
theMatchUrl, RuntimeResourceDefinition resourceDef) {
SearchParameterMap paramMap = new SearchParameterMap();
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
for (NameValuePair next : parameters) {
if (isBlank(next.getValue())) {
continue;
}
String paramName = next.getName();
String qualifier = null;
for (int i = 0; i < paramName.length(); i++) {
switch (paramName.charAt(i)) {
case '.':
case ':':
qualifier = paramName.substring(i);
paramName = paramName.substring(0, i);
i = Integer.MAX_VALUE - 1;
break;
}
}
QualifiedParamList paramList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
nameToParamLists.put(paramName, paramList);
}
for (String nextParamName : nameToParamLists.keySet()) {
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
if (paramList != null && paramList.size() > 0) {
if (paramList.size() > 2) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED + " parameter repetitions");
} else {
DateRangeParam p1 = new DateRangeParam();
p1.setValuesAsQueryTokens(myContext, nextParamName, paramList);
paramMap.setLastUpdated(p1);
}
}
continue;
}
if (Constants.PARAM_HAS.equals(nextParamName)) {
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(myContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
paramMap.add(nextParamName, param);
continue;
}
if (Constants.PARAM_COUNT.equals(nextParamName)) {
if (paramList.size() > 0 && paramList.get(0).size() > 0) {
String intString = paramList.get(0).get(0);
try {
paramMap.setCount(Integer.parseInt(intString));
} catch (NumberFormatException e) {
throw new InvalidRequestException("Invalid " + Constants.PARAM_COUNT + " value: " + intString);
}
}
continue;
}
if (ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(nextParamName)) {
if (isNotBlank(paramList.get(0).getQualifier()) && paramList.get(0).getQualifier().startsWith(".")) {
throw new InvalidRequestException("Invalid parameter chain: " + nextParamName + paramList.get(0).getQualifier());
}
IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
type.setValuesAsQueryTokens(myContext, nextParamName, (paramList));
paramMap.add(nextParamName, type);
} else if (nextParamName.startsWith("_")) {
// ignore these since they aren't search params (e.g. _sort)
} else {
RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(resourceDef, nextParamName);
if (paramDef == null) {
throw new InvalidRequestException(
"Failed to parse match URL[" + theMatchUrl + "] - Resource type " + resourceDef.getName() + " does not have a parameter with name: " + nextParamName);
}
IQueryParameterAnd<?> param = ParameterUtil.parseQueryParams(myContext, paramDef, nextParamName, paramList);
paramMap.add(nextParamName, param);
}
}
return paramMap;
}
public List<NameValuePair> translateMatchUrl(String theMatchUrl) {
List<NameValuePair> parameters;
String matchUrl = theMatchUrl;
int questionMarkIndex = matchUrl.indexOf('?');
if (questionMarkIndex != -1) {
matchUrl = matchUrl.substring(questionMarkIndex + 1);
}
matchUrl = matchUrl.replace("|", "%7C");
matchUrl = matchUrl.replace("=>=", "=%3E%3D");
matchUrl = matchUrl.replace("=<=", "=%3C%3D");
matchUrl = matchUrl.replace("=>", "=%3E");
matchUrl = matchUrl.replace("=<", "=%3C");
if (matchUrl.contains(" ")) {
throw new InvalidRequestException("Failed to parse match URL[" + theMatchUrl + "] - URL is invalid (must not contain spaces)");
}
parameters = URLEncodedUtils.parse((matchUrl), Constants.CHARSET_UTF8, '&');
return parameters;
}
@CoverageIgnore
protected IQueryParameterAnd newInstanceAnd(String chain) {
IQueryParameterAnd type;
Class<? extends IQueryParameterAnd> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
@CoverageIgnore
public IQueryParameterType newInstanceType(String chain) {
IQueryParameterType type;
Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(chain);
try {
type = clazz.newInstance();
} catch (Exception e) {
throw new InternalErrorException("Failure creating instance of " + clazz, e);
}
return type;
}
}

View File

@ -0,0 +1,43 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.*;
import org.hl7.fhir.r4.model.BaseResource;
import java.util.*;
public class ResourceMetaParams {
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
public static final Map<String, Class<? extends IQueryParameterAnd<?>>> RESOURCE_META_AND_PARAMS;
/**
* These are parameters which are supported by {@link BaseHapiFhirResourceDao#searchForIds(SearchParameterMap)}
*/
public static final Map<String, Class<? extends IQueryParameterType>> RESOURCE_META_PARAMS;
public static final Set<String> EXCLUDE_ELEMENTS_IN_ENCODED;
static {
Map<String, Class<? extends IQueryParameterType>> resourceMetaParams = new HashMap<String, Class<? extends IQueryParameterType>>();
Map<String, Class<? extends IQueryParameterAnd<?>>> resourceMetaAndParams = new HashMap<String, Class<? extends IQueryParameterAnd<?>>>();
resourceMetaParams.put(BaseResource.SP_RES_ID, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_ID, StringAndListParam.class);
resourceMetaParams.put(BaseResource.SP_RES_LANGUAGE, StringParam.class);
resourceMetaAndParams.put(BaseResource.SP_RES_LANGUAGE, StringAndListParam.class);
resourceMetaParams.put(Constants.PARAM_TAG, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_TAG, TokenAndListParam.class);
resourceMetaParams.put(Constants.PARAM_PROFILE, UriParam.class);
resourceMetaAndParams.put(Constants.PARAM_PROFILE, UriAndListParam.class);
resourceMetaParams.put(Constants.PARAM_SECURITY, TokenParam.class);
resourceMetaAndParams.put(Constants.PARAM_SECURITY, TokenAndListParam.class);
RESOURCE_META_PARAMS = Collections.unmodifiableMap(resourceMetaParams);
RESOURCE_META_AND_PARAMS = Collections.unmodifiableMap(resourceMetaAndParams);
HashSet<String> excludeElementsInEncoded = new HashSet<String>();
excludeElementsInEncoded.add("id");
excludeElementsInEncoded.add("*.meta");
EXCLUDE_ELEMENTS_IN_ENCODED = Collections.unmodifiableSet(excludeElementsInEncoded);
}
}

View File

@ -21,10 +21,11 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao;
import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
@ -69,10 +70,15 @@ import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPre
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.math.BigDecimal;
@ -87,6 +93,8 @@ import static org.apache.commons.lang3.StringUtils.*;
* searches for resources
*/
@SuppressWarnings("JpaQlInspection")
@Component
@Scope("prototype")
public class SearchBuilder implements ISearchBuilder {
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
@ -97,26 +105,42 @@ public class SearchBuilder implements ISearchBuilder {
private static String ourLastHandlerThreadForUnitTest;
private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch;
private final DaoConfig myDaoConfig;
@Autowired
protected IResourceTagDao myResourceTagDao;
@Autowired
private IResourceSearchViewDao myResourceSearchViewDao;
@Autowired
private FhirContext myContext;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
private IdHelperService myIdHelperService;
@Autowired(required = false)
private IFulltextSearchSvc myFulltextSearchSvc;
@Autowired
private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao;
private FhirContext myContext;
private EntityManager myEntityManager;
private IForcedIdDao myForcedIdDao;
private IFulltextSearchSvc myFulltextSearchSvc;
private Map<JoinKey, Join<?, ?>> myIndexJoins = Maps.newHashMap();
private SearchParameterMap myParams;
private ArrayList<Predicate> myPredicates;
private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
private String myResourceName;
private AbstractQuery<Long> myResourceTableQuery;
private Root<ResourceTable> myResourceTableRoot;
private Class<? extends IBaseResource> myResourceType;
private ISearchParamRegistry mySearchParamRegistry;
private String mySearchUuid;
private IHapiTerminologySvc myTerminologySvc;
private int myFetchSize;
private Integer myMaxResultsToFetch;
private Set<Long> myPidSet;
@ -124,22 +148,10 @@ public class SearchBuilder implements ISearchBuilder {
/**
* Constructor
*/
SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager,
IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao<?> theDao,
IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao,
IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry,
IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) {
myContext = theFhirContext;
myEntityManager = theEntityManager;
myFulltextSearchSvc = theFulltextSearchSvc;
SearchBuilder(BaseHapiFhirDao<?> theDao) {
myCallingDao = theDao;
myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches();
myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao;
myForcedIdDao = theForcedIdDao;
myTerminologySvc = theTerminologySvc;
mySearchParamRegistry = theSearchParamRegistry;
myResourceTagDao = theResourceTagDao;
myResourceSearchViewDao = theResourceViewDao;
myDaoConfig = theDao.getConfig();
myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches();
}
@Override
@ -220,18 +232,18 @@ public class SearchBuilder implements ISearchBuilder {
assert parameterName != null;
String paramName = parameterName.replaceAll("\\..*", "");
RuntimeSearchParam owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, paramName);
RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName);
if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName);
}
owningParameterDef = myCallingDao.getSearchParamByName(targetResourceDefinition, owningParameter);
owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, owningParameter);
if (owningParameterDef == null) {
throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + owningParameter);
}
Class<? extends IBaseResource> resourceType = targetResourceDefinition.getImplementingClass();
Set<Long> match = myCallingDao.processMatchUrl(matchUrl, resourceType);
Set<Long> match = myMatchUrlService.processMatchUrl(matchUrl, resourceType);
if (match.isEmpty()) {
// Pick a PID that can never match
match = Collections.singleton(-1L);
@ -373,7 +385,7 @@ public class SearchBuilder implements ISearchBuilder {
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
if (dt.hasBaseUrl()) {
if (myCallingDao.getConfig().getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
dt = dt.toUnqualified();
} else {
ourLog.debug("Searching for resource link with target URL: {}", dt.getValue());
@ -385,7 +397,7 @@ public class SearchBuilder implements ISearchBuilder {
List<Long> targetPid;
try {
targetPid = myCallingDao.translateForcedIdToPids(dt);
targetPid = myIdHelperService.translateForcedIdToPids(dt);
} catch (ResourceNotFoundException e) {
// Use a PID that will never exist
targetPid = Collections.singletonList(-1L);
@ -417,7 +429,7 @@ public class SearchBuilder implements ISearchBuilder {
if (resourceTypes.isEmpty()) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
RuntimeSearchParam searchParamByName = myCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (searchParamByName == null) {
throw new InternalErrorException("Could not find parameter " + theParamName);
}
@ -491,10 +503,10 @@ public class SearchBuilder implements ISearchBuilder {
chain = chain.substring(0, qualifierIndex);
}
boolean isMeta = BaseHapiFhirDao.RESOURCE_META_PARAMS.containsKey(chain);
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
RuntimeSearchParam param = null;
if (!isMeta) {
param = myCallingDao.getSearchParamByName(typeDef, chain);
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
if (param == null) {
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
continue;
@ -512,7 +524,7 @@ public class SearchBuilder implements ISearchBuilder {
chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
((ReferenceParam) chainValue).setChain(remainingChain);
} else if (isMeta) {
IQueryParameterType type = BaseHapiFhirDao.newInstanceType(chain);
IQueryParameterType type = myMatchUrlService.newInstanceType(chain);
type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
chainValue = type;
} else {
@ -1162,7 +1174,6 @@ public class SearchBuilder implements ISearchBuilder {
private Predicate createPredicateString(IQueryParameterType theParameter, String theResourceName, String theParamName, CriteriaBuilder theBuilder,
From<?, ResourceIndexedSearchParamString> theFrom) {
String rawSearchTerm;
DaoConfig daoConfig = myCallingDao.getConfig();
if (theParameter instanceof TokenParam) {
TokenParam id = (TokenParam) theParameter;
if (!id.isText()) {
@ -1173,7 +1184,7 @@ public class SearchBuilder implements ISearchBuilder {
StringParam id = (StringParam) theParameter;
rawSearchTerm = id.getValue();
if (id.isContains()) {
if (!daoConfig.isAllowContainsSearches()) {
if (!myDaoConfig.isAllowContainsSearches()) {
throw new MethodNotAllowedException(":contains modifier is disabled on this server");
}
}
@ -1191,7 +1202,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myDontUseHashesForSearch) {
String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm);
if (myCallingDao.getConfig().isAllowContainsSearches()) {
if (myDaoConfig.isAllowContainsSearches()) {
if (theParameter instanceof StringParam) {
if (((StringParam) theParameter).isContains()) {
likeExpression = createLeftAndRightMatchLikeExpression(likeExpression);
@ -1230,13 +1241,13 @@ public class SearchBuilder implements ISearchBuilder {
String likeExpression;
if (theParameter instanceof StringParam &&
((StringParam) theParameter).isContains() &&
daoConfig.isAllowContainsSearches()) {
myDaoConfig.isAllowContainsSearches()) {
likeExpression = createLeftAndRightMatchLikeExpression(normalizedString);
} else {
likeExpression = createLeftMatchLikeExpression(normalizedString);
}
Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(daoConfig, theResourceName, theParamName, normalizedString);
Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig, theResourceName, theParamName, normalizedString);
Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash);
Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression);
return theBuilder.and(hashCode, singleCode);
@ -1473,7 +1484,7 @@ public class SearchBuilder implements ISearchBuilder {
* of parameters passed in
*/
ourLog.debug("Checking for unique index for query: {}", theParams.toNormalizedQueryString(myContext));
if (myCallingDao.getConfig().isUniqueIndexesEnabled()) {
if (myDaoConfig.isUniqueIndexesEnabled()) {
if (myParams.getIncludes().isEmpty()) {
if (myParams.getRevIncludes().isEmpty()) {
if (myParams.getEverythingMode() == null) {
@ -1600,7 +1611,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myParams.get(IAnyResource.SP_RES_ID) != null) {
StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
Long pid = BaseHapiFhirDao.translateForcedIdToPid(myCallingDao.getConfig(), myResourceName, idParm.getValue(), myForcedIdDao);
Long pid = myIdHelperService.translateForcedIdToPid(myResourceName, idParm.getValue());
if (myAlsoIncludePids == null) {
myAlsoIncludePids = new ArrayList<>(1);
}
@ -1677,7 +1688,7 @@ public class SearchBuilder implements ISearchBuilder {
}
private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From<?, ? extends ResourceLink> from) {
return createResourceLinkPathPredicate(myCallingDao, myContext, theParamName, from, theResourceName);
return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName);
}
/**
@ -1713,7 +1724,7 @@ public class SearchBuilder implements ISearchBuilder {
}
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theSort.getParamName());
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theSort.getParamName());
if (param == null) {
throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'");
}
@ -1807,7 +1818,7 @@ public class SearchBuilder implements ISearchBuilder {
String retVal = theSystem;
if (retVal == null) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName);
RuntimeSearchParam param = myCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (param != null) {
Set<String> valueSetUris = Sets.newHashSet();
for (String nextPath : param.getPathsSplit()) {
@ -1957,7 +1968,7 @@ public class SearchBuilder implements ISearchBuilder {
* so it can't be Collections.emptySet() or some such thing
*/
@Override
public HashSet<Long> loadIncludes(IDao theCallingDao, FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
public HashSet<Long> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<Long> theMatches, Set<Include> theRevIncludes,
boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription) {
if (theMatches.size() == 0) {
return new HashSet<>();
@ -2017,7 +2028,7 @@ public class SearchBuilder implements ISearchBuilder {
String paramName = nextInclude.getParamName();
if (isNotBlank(paramName)) {
param = theCallingDao.getSearchParamByName(def, paramName);
param = mySearchParamRegistry.getSearchParamByName(def, paramName);
} else {
param = null;
}
@ -2088,6 +2099,7 @@ public class SearchBuilder implements ISearchBuilder {
private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams) {
myParams = theParams;
theParams.clean();
for (Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
String nextParamName = nextParamEntry.getKey();
List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue();
@ -2099,37 +2111,6 @@ public class SearchBuilder implements ISearchBuilder {
private void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
/*
* Filter out
*/
for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) {
List<? extends IQueryParameterType> nextOrList = theAndOrParams.get(andListIdx);
for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) {
IQueryParameterType nextOr = nextOrList.get(orListIdx);
boolean hasNoValue = false;
if (nextOr.getMissing() != null) {
continue;
}
if (nextOr instanceof QuantityParam) {
if (isBlank(((QuantityParam) nextOr).getValueAsString())) {
hasNoValue = true;
}
}
if (hasNoValue) {
ourLog.debug("Ignoring empty parameter: {}", theParamName);
nextOrList.remove(orListIdx);
orListIdx--;
}
}
if (nextOrList.isEmpty()) {
theAndOrParams.remove(andListIdx);
andListIdx--;
}
}
if (theAndOrParams.isEmpty()) {
return;
}
@ -2288,7 +2269,7 @@ public class SearchBuilder implements ISearchBuilder {
private int myCurrentOffset;
private ArrayList<Long> myCurrentPids;
private Long myNext;
private int myPageSize = myCallingDao.getConfig().getEverythingIncludesFetchPageSize();
private int myPageSize = myDaoConfig.getEverythingIncludesFetchPageSize();
IncludesIterator(Set<Long> thePidSet) {
myCurrentPids = new ArrayList<>(thePidSet);
@ -2316,7 +2297,7 @@ public class SearchBuilder implements ISearchBuilder {
myCurrentOffset = end;
Collection<Long> pidsToScan = myCurrentPids.subList(start, end);
Set<Include> includes = Collections.singleton(new Include("*", true));
Set<Long> newPids = loadIncludes(myCallingDao, myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid);
Set<Long> newPids = loadIncludes(myContext, myEntityManager, pidsToScan, includes, false, myParams.getLastUpdated(), mySearchUuid);
myCurrentIterator = newPids.iterator();
}
@ -2368,7 +2349,7 @@ public class SearchBuilder implements ISearchBuilder {
// If we don't have a query yet, create one
if (myResultsIterator == null) {
if (myMaxResultsToFetch == null) {
myMaxResultsToFetch = myCallingDao.getConfig().getFetchSizeDefaultMaximum();
myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum();
}
final TypedQuery<Long> query = createQuery(mySort, myMaxResultsToFetch, false);
@ -2483,7 +2464,7 @@ public class SearchBuilder implements ISearchBuilder {
if (myWrap == null) {
ourLog.debug("Searching for unique index matches over {} candidate query strings", myUniqueQueryStrings.size());
StopWatch sw = new StopWatch();
Collection<Long> resourcePids = myCallingDao.getResourceIndexedCompositeStringUniqueDao().findResourcePidsByQueryStrings(myUniqueQueryStrings);
Collection<Long> resourcePids = myResourceIndexedCompositeStringUniqueDao.findResourcePidsByQueryStrings(myUniqueQueryStrings);
ourLog.debug("Found {} unique index matches in {}ms", resourcePids.size(), sw.getMillis());
myWrap = resourcePids.iterator();
}
@ -2621,10 +2602,10 @@ public class SearchBuilder implements ISearchBuilder {
return likeExpression.replace("%", "[%]") + "%";
}
private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
String theResourceType) {
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName);
RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
List<String> path = param.getPathsSplit();
/*

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.util.ObjectUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.StringUtils;
@ -17,6 +18,7 @@ import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
@ -40,6 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class SearchParameterMap extends LinkedHashMap<String, List<List<? extends IQueryParameterType>>> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class);
private static final long serialVersionUID = 1L;
@ -336,6 +339,10 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
IQueryParameterType firstValue = nextValuesAnd.get(0);
b.append(UrlUtil.escapeUrlParam(nextKey));
if (nextKey.equals(Constants.PARAM_HAS)) {
b.append(':');
}
if (firstValue.getMissing() != null) {
b.append(Constants.PARAMQUALIFIER_MISSING);
b.append('=');
@ -433,6 +440,47 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
return b.toString();
}
public void clean() {
for (Map.Entry<String, List<List<? extends IQueryParameterType>>> nextParamEntry : this.entrySet()) {
String nextParamName = nextParamEntry.getKey();
List<List<? extends IQueryParameterType>> andOrParams = nextParamEntry.getValue();
clean(nextParamName, andOrParams);
}
}
/*
* Filter out
*/
private void clean(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams) {
for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) {
List<? extends IQueryParameterType> nextOrList = theAndOrParams.get(andListIdx);
for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) {
IQueryParameterType nextOr = nextOrList.get(orListIdx);
boolean hasNoValue = false;
if (nextOr.getMissing() != null) {
continue;
}
if (nextOr instanceof QuantityParam) {
if (isBlank(((QuantityParam) nextOr).getValueAsString())) {
hasNoValue = true;
}
}
if (hasNoValue) {
ourLog.debug("Ignoring empty parameter: {}", theParamName);
nextOrList.remove(orListIdx);
orListIdx--;
}
}
if (nextOrList.isEmpty()) {
theAndOrParams.remove(andListIdx);
andListIdx--;
}
}
}
public enum EverythingModeEnum {
/*
* Don't reorder! We rely on the ordinals

View File

@ -43,7 +43,10 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
@ -81,6 +84,8 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
@Autowired
private ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> myVersionAdapter;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private DaoRegistry myDaoRegistry;
private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) {
@ -390,7 +395,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
requestDetails.setParameters(new HashMap<>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = BaseHapiFhirDao.translateMatchUrl(params);
List<NameValuePair> parameters = myMatchUrlService.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}

View File

@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
@ -65,9 +64,6 @@ public class FhirResourceDaoCodeSystemDstu3 extends FhirResourceDaoDstu3<CodeSys
@Autowired
private ITermCodeSystemDao myCsDao;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private ValidationSupportChain myValidationSupport;

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class IdHelperService {
@Autowired
protected IForcedIdDao myForcedIdDao;
@Autowired(required = true)
private DaoConfig myDaoConfig;
public void delete(ForcedId forcedId) {
myForcedIdDao.delete(forcedId);
}
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
return translateForcedIdToPids(myDaoConfig, new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
}
public List<Long> translateForcedIdToPids(IIdType theId) {
return IdHelperService.translateForcedIdToPids(myDaoConfig, theId, myForcedIdDao);
}
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
Validate.isTrue(theId.hasIdPart());
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
return Collections.singletonList(theId.getIdPartAsLong());
} else {
List<ForcedId> forcedId;
if (theId.hasResourceType()) {
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
} else {
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
}
if (!forcedId.isEmpty()) {
List<Long> retVal = new ArrayList<>(forcedId.size());
for (ForcedId next : forcedId) {
retVal.add(next.getResourcePid());
}
return retVal;
} else {
throw new ResourceNotFoundException(theId);
}
}
}
public String translatePidIdToForcedId(String theResourceType, Long theId) {
ForcedId forcedId = myForcedIdDao.findByResourcePid(theId);
if (forcedId != null) {
return forcedId.getResourceType() + '/' + forcedId.getForcedId();
} else {
return theResourceType + '/' + theId.toString();
}
}
public static boolean isValidPid(IIdType theId) {
if (theId == null || theId.getIdPart() == null) {
return false;
}
String idPart = theId.getIdPart();
for (int i = 0; i < idPart.length(); i++) {
char nextChar = idPart.charAt(i);
if (nextChar < '0' || nextChar > '9') {
return false;
}
}
return true;
}
}

View File

@ -1,58 +0,0 @@
package ca.uhn.fhir.jpa.dao.index;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import ca.uhn.fhir.jpa.entity.BaseHasResource;
import ca.uhn.fhir.jpa.entity.IBaseResourceEntity;
import ca.uhn.fhir.jpa.entity.ResourceTag;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.ISearchParamExtractor;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
public interface IndexingSupport {
public DaoConfig getConfig();
public ISearchParamExtractor getSearchParamExtractor();
public ISearchParamRegistry getSearchParamRegistry();
public FhirContext getContext();
public EntityManager getEntityManager();
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType);
public Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> getResourceTypeToDao();
public boolean isLogicalReference(IIdType nextId);
public IForcedIdDao getForcedIdDao();
public <R extends IBaseResource> Set<Long> processMatchUrl(String theMatchUrl, Class<R> theResourceType);
public Long translateForcedIdToPid(String theResourceName, String theResourceId);
public String toResourceName(Class<? extends IBaseResource> theResourceType);
public IResourceIndexedCompositeStringUniqueDao getResourceIndexedCompositeStringUniqueDao();
}

View File

@ -20,334 +20,78 @@ package ca.uhn.fhir.jpa.dao.index;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.compare;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.persistence.EntityManager;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Reference;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.PathAndRef;
import ca.uhn.fhir.jpa.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.entity.ForcedId;
import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamCoords;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.entity.ResourceLink;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import static org.apache.commons.lang3.StringUtils.*;
public class ResourceIndexedSearchParams {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceIndexedSearchParams.class);
// FIXME rename
private final IndexingSupport myIndexingService;
final Collection<ResourceIndexedSearchParamString> stringParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamToken> tokenParams = new HashSet<>();
final Collection<ResourceIndexedSearchParamNumber> numberParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamQuantity> quantityParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamDate> dateParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamUri> uriParams = new ArrayList<>();
final Collection<ResourceIndexedSearchParamCoords> coordsParams = new ArrayList<>();
private final Collection<ResourceIndexedSearchParamString> stringParams;
private final Collection<ResourceIndexedSearchParamToken> tokenParams;
private final Collection<ResourceIndexedSearchParamNumber> numberParams;
private final Collection<ResourceIndexedSearchParamQuantity> quantityParams;
private final Collection<ResourceIndexedSearchParamDate> dateParams;
private final Collection<ResourceIndexedSearchParamUri> uriParams;
private final Collection<ResourceIndexedSearchParamCoords> coordsParams;
private final Collection<ResourceIndexedCompositeStringUnique> compositeStringUniques;
private final Collection<ResourceLink> links;
final Collection<ResourceIndexedCompositeStringUnique> compositeStringUniques = new HashSet<>();
final Collection<ResourceLink> links = new HashSet<>();
final Set<String> populatedResourceLinkParameters = new HashSet<>();
private Set<String> populatedResourceLinkParameters = Collections.emptySet();
public ResourceIndexedSearchParams(IndexingSupport indexingService, ResourceTable theEntity) {
this.myIndexingService = indexingService;
public ResourceIndexedSearchParams() {
}
stringParams = new ArrayList<>();
public ResourceIndexedSearchParams(ResourceTable theEntity) {
if (theEntity.isParamsStringPopulated()) {
stringParams.addAll(theEntity.getParamsString());
}
tokenParams = new ArrayList<>();
if (theEntity.isParamsTokenPopulated()) {
tokenParams.addAll(theEntity.getParamsToken());
}
numberParams = new ArrayList<>();
if (theEntity.isParamsNumberPopulated()) {
numberParams.addAll(theEntity.getParamsNumber());
}
quantityParams = new ArrayList<>();
if (theEntity.isParamsQuantityPopulated()) {
quantityParams.addAll(theEntity.getParamsQuantity());
}
dateParams = new ArrayList<>();
if (theEntity.isParamsDatePopulated()) {
dateParams.addAll(theEntity.getParamsDate());
}
uriParams = new ArrayList<>();
if (theEntity.isParamsUriPopulated()) {
uriParams.addAll(theEntity.getParamsUri());
}
coordsParams = new ArrayList<>();
if (theEntity.isParamsCoordsPopulated()) {
coordsParams.addAll(theEntity.getParamsCoords());
}
links = new ArrayList<>();
if (theEntity.isHasLinks()) {
links.addAll(theEntity.getResourceLinks());
}
compositeStringUniques = new ArrayList<>();
if (theEntity.isParamsCompositeStringUniquePresent()) {
compositeStringUniques.addAll(theEntity.getParamsCompositeStringUnique());
}
}
public ResourceIndexedSearchParams(IndexingSupport indexingService) {
this.myIndexingService = indexingService;
stringParams = Collections.emptySet();
tokenParams = Collections.emptySet();
numberParams = Collections.emptySet();
quantityParams = Collections.emptySet();
dateParams = Collections.emptySet();
uriParams = Collections.emptySet();
coordsParams = Collections.emptySet();
links = Collections.emptySet();
compositeStringUniques = Collections.emptySet();
}
public ResourceIndexedSearchParams(IndexingSupport indexingService, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) {
this.myIndexingService = indexingService;
stringParams = extractSearchParamStrings(theEntity, theResource);
numberParams = extractSearchParamNumber(theEntity, theResource);
quantityParams = extractSearchParamQuantity(theEntity, theResource);
dateParams = extractSearchParamDates(theEntity, theResource);
uriParams = extractSearchParamUri(theEntity, theResource);
coordsParams = extractSearchParamCoords(theEntity, theResource);
ourLog.trace("Storing date indexes: {}", dateParams);
tokenParams = new HashSet<>();
for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) {
if (next instanceof ResourceIndexedSearchParamToken) {
tokenParams.add((ResourceIndexedSearchParamToken) next);
} else {
stringParams.add((ResourceIndexedSearchParamString) next);
}
}
Set<Entry<String, RuntimeSearchParam>> activeSearchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(theEntity.getResourceType()).entrySet();
DaoConfig myConfig = indexingService.getConfig();
if (myConfig .getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.STRING, stringParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.DATE, dateParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.URI, uriParams);
findMissingSearchParams(theEntity, activeSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams);
}
setUpdatedTime(stringParams, theUpdateTime);
setUpdatedTime(numberParams, theUpdateTime);
setUpdatedTime(quantityParams, theUpdateTime);
setUpdatedTime(dateParams, theUpdateTime);
setUpdatedTime(uriParams, theUpdateTime);
setUpdatedTime(coordsParams, theUpdateTime);
setUpdatedTime(tokenParams, theUpdateTime);
/*
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
* matching resource.
*/
if (myConfig.isAllowInlineMatchUrlReferences()) {
FhirTerser terser = myIndexingService.getContext().newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myIndexingService.getContext().getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
Set<Long> matches = myIndexingService.processMatchUrl(nextIdText, matchResourceType);
if (matches.isEmpty()) {
String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(msg);
}
if (matches.size() > 1) {
String msg = indexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(msg);
}
Long next = matches.iterator().next();
String newId = translatePidIdToForcedId(resourceTypeString, next);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
nextRef.setReference(newId);
}
}
}
links = new HashSet<>();
populatedResourceLinkParameters = extractResourceLinks(theEntity, theResource, links, theUpdateTime);
/*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
*/
for (Iterator<ResourceLink> existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
ResourceLink nextExisting = existingLinkIter.next();
if (links.remove(nextExisting)) {
existingLinkIter.remove();
links.add(nextExisting);
}
}
/*
* Handle composites
*/
compositeStringUniques = extractCompositeStringUniques(theEntity, stringParams, tokenParams, numberParams, quantityParams, dateParams, uriParams, links);
}
public Collection<ResourceLink> getResourceLinks() {
return links;
}
protected Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamCoords(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamDates(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamNumber(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamQuantity(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamStrings(theEntity, theResource);
}
protected Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamTokens(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
return myIndexingService.getSearchParamExtractor().extractSearchParamUri(theEntity, theResource);
}
@SuppressWarnings("unchecked")
private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(ResourceTable theEntity, Set<Entry<String, RuntimeSearchParam>> activeSearchParams, RestSearchParameterTypeEnum type,
Collection<RT> paramCollection) {
for (Entry<String, RuntimeSearchParam> nextEntry : activeSearchParams) {
String nextParamName = nextEntry.getKey();
if (nextEntry.getValue().getParamType() == type) {
boolean haveParam = false;
for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
if (nextParam.getParamName().equals(nextParamName)) {
haveParam = true;
break;
}
}
if (!haveParam) {
BaseResourceIndexedSearchParam param;
switch (type) {
case DATE:
param = new ResourceIndexedSearchParamDate();
break;
case NUMBER:
param = new ResourceIndexedSearchParamNumber();
break;
case QUANTITY:
param = new ResourceIndexedSearchParamQuantity();
break;
case STRING:
param = new ResourceIndexedSearchParamString()
.setDaoConfig(myIndexingService.getConfig());
break;
case TOKEN:
param = new ResourceIndexedSearchParamToken();
break;
case URI:
param = new ResourceIndexedSearchParamUri();
break;
case COMPOSITE:
case HAS:
case REFERENCE:
default:
continue;
}
param.setResource(theEntity);
param.setMissing(true);
param.setParamName(nextParamName);
paramCollection.add((RT) param);
}
}
}
}
public void setParams(ResourceTable theEntity) {
public void setParamsOn(ResourceTable theEntity) {
theEntity.setParamsString(stringParams);
theEntity.setParamsStringPopulated(stringParams.isEmpty() == false);
theEntity.setParamsToken(tokenParams);
@ -367,88 +111,22 @@ public class ResourceIndexedSearchParams {
theEntity.setHasLinks(links.isEmpty() == false);
}
private Set<ResourceIndexedCompositeStringUnique> extractCompositeStringUniques(ResourceTable theEntity, Collection<ResourceIndexedSearchParamString> theStringParams, Collection<ResourceIndexedSearchParamToken> theTokenParams, Collection<ResourceIndexedSearchParamNumber> theNumberParams, Collection<ResourceIndexedSearchParamQuantity> theQuantityParams, Collection<ResourceIndexedSearchParamDate> theDateParams, Collection<ResourceIndexedSearchParamUri> theUriParams, Collection<ResourceLink> theLinks) {
Set<ResourceIndexedCompositeStringUnique> compositeStringUniques;
compositeStringUniques = new HashSet<>();
List<JpaRuntimeSearchParam> uniqueSearchParams = myIndexingService.getSearchParamRegistry().getActiveUniqueSearchParams(theEntity.getResourceType());
for (JpaRuntimeSearchParam next : uniqueSearchParams) {
List<List<String>> partsChoices = new ArrayList<>();
for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
Collection<ResourceLink> linksForCompositePart = null;
Collection<String> linksForCompositePartWantPaths = null;
switch (nextCompositeOf.getParamType()) {
case NUMBER:
paramsListForCompositePart = theNumberParams;
break;
case DATE:
paramsListForCompositePart = theDateParams;
break;
case STRING:
paramsListForCompositePart = theStringParams;
break;
case TOKEN:
paramsListForCompositePart = theTokenParams;
break;
case REFERENCE:
linksForCompositePart = theLinks;
linksForCompositePartWantPaths = new HashSet<>();
linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit());
break;
case QUANTITY:
paramsListForCompositePart = theQuantityParams;
break;
case URI:
paramsListForCompositePart = theUriParams;
break;
case COMPOSITE:
case HAS:
break;
public void setUpdatedTime(Date theUpdateTime) {
setUpdatedTime(stringParams, theUpdateTime);
setUpdatedTime(numberParams, theUpdateTime);
setUpdatedTime(quantityParams, theUpdateTime);
setUpdatedTime(dateParams, theUpdateTime);
setUpdatedTime(uriParams, theUpdateTime);
setUpdatedTime(coordsParams, theUpdateTime);
setUpdatedTime(tokenParams, theUpdateTime);
}
ArrayList<String> nextChoicesList = new ArrayList<>();
partsChoices.add(nextChoicesList);
String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
if (paramsListForCompositePart != null) {
for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
String value = nextParamAsClientParam.getValueAsQueryToken(myIndexingService.getContext());
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
if (linksForCompositePart != null) {
for (ResourceLink nextLink : linksForCompositePart) {
if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
nextSearchParam.setUpdated(theUpdateTime);
}
}
Set<String> queryStringsToPopulate = extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices);
for (String nextQueryString : queryStringsToPopulate) {
if (isNotBlank(nextQueryString)) {
compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
}
}
}
return compositeStringUniques;
}
/**
* This method is used to create a set of all possible combinations of
@ -531,342 +209,163 @@ public class ResourceIndexedSearchParams {
}
/**
* @return Returns a set containing all of the parameter names that
* were found to have a value
*/
@SuppressWarnings("unchecked")
protected Set<String> extractResourceLinks(ResourceTable theEntity, IBaseResource theResource, Collection<ResourceLink> theLinks, Date theUpdateTime) {
HashSet<String> retVal = new HashSet<>();
String resourceType = theEntity.getResourceType();
/*
* For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing..
*/
if (theResource instanceof IBaseBundle) {
return Collections.emptySet();
}
Map<String, RuntimeSearchParam> searchParams = myIndexingService.getSearchParamRegistry().getActiveSearchParams(myIndexingService.toResourceName(theResource.getClass()));
for (RuntimeSearchParam nextSpDef : searchParams.values()) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
continue;
}
String nextPathsUnsplit = nextSpDef.getPath();
if (isBlank(nextPathsUnsplit)) {
continue;
}
boolean multiType = false;
if (nextPathsUnsplit.endsWith("[x]")) {
multiType = true;
}
List<PathAndRef> refs = myIndexingService.getSearchParamExtractor().extractResourceLinks(theResource, nextSpDef);
for (PathAndRef nextPathAndRef : refs) {
Object nextObject = nextPathAndRef.getRef();
/*
* A search parameter on an extension field that contains
* references should index those references
*/
if (nextObject instanceof IBaseExtension<?, ?>) {
nextObject = ((IBaseExtension<?, ?>) nextObject).getValue();
}
if (nextObject instanceof CanonicalType) {
nextObject = new Reference(((CanonicalType) nextObject).getValueAsString());
}
IIdType nextId;
if (nextObject instanceof IBaseReference) {
IBaseReference nextValue = (IBaseReference) nextObject;
if (nextValue.isEmpty()) {
continue;
}
nextId = nextValue.getReferenceElement();
/*
* This can only really happen if the DAO is being called
* programatically with a Bundle (not through the FHIR REST API)
* but Smile does this
*/
if (nextId.isEmpty() && nextValue.getResource() != null) {
nextId = nextValue.getResource().getIdElement();
}
if (nextId.isEmpty() || nextId.getValue().startsWith("#")) {
// This is a blank or contained resource reference
continue;
}
} else if (nextObject instanceof IBaseResource) {
nextId = ((IBaseResource) nextObject).getIdElement();
if (nextId == null || nextId.hasIdPart() == false) {
continue;
}
} else if (myIndexingService.getContext().getElementDefinition((Class<? extends IBase>) nextObject.getClass()).getName().equals("uri")) {
continue;
} else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) {
// Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
continue;
} else {
if (!multiType) {
if (nextSpDef.getName().equals("sourceuri")) {
continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI
}
throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
} else {
continue;
}
}
retVal.add(nextSpDef.getName());
if (myIndexingService.isLogicalReference(nextId)) {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
String baseUrl = nextId.getBaseUrl();
String typeString = nextId.getResourceType();
if (isBlank(typeString)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue());
}
RuntimeResourceDefinition resourceDefinition;
try {
resourceDefinition = myIndexingService.getContext().getResourceDefinition(typeString);
} catch (DataFormatException e) {
throw new InvalidRequestException(
"Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue());
}
if (isNotBlank(baseUrl)) {
if (!myIndexingService.getConfig().getTreatBaseUrlsAsLocal().contains(baseUrl) && !myIndexingService.getConfig().isAllowExternalReferences()) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue());
throw new InvalidRequestException(msg);
} else {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theLinks.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
}
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue());
}
IFhirResourceDao<?> dao = myIndexingService.getDao(type);
if (dao == null) {
StringBuilder b = new StringBuilder();
b.append("This server (version ");
b.append(myIndexingService.getContext().getVersion().getVersion());
b.append(") is not able to handle resources of type[");
b.append(nextId.getResourceType());
b.append("] - Valid resource types for this server: ");
b.append(myIndexingService.getResourceTypeToDao().keySet().toString());
throw new InvalidRequestException(b.toString());
}
Long valueOf;
try {
valueOf = myIndexingService.translateForcedIdToPid(typeString, id);
} catch (ResourceNotFoundException e) {
if (myIndexingService.getConfig().isEnforceReferentialIntegrityOnWrite() == false) {
continue;
}
RuntimeResourceDefinition missingResourceDef = myIndexingService.getContext().getResourceDefinition(type);
String resName = missingResourceDef.getName();
if (myIndexingService.getConfig().isAutoCreatePlaceholderReferenceTargets()) {
IBaseResource newResource = missingResourceDef.newInstance();
newResource.setId(resName + "/" + id);
IFhirResourceDao<IBaseResource> placeholderResourceDao = (IFhirResourceDao<IBaseResource>) myIndexingService.getDao(newResource.getClass());
ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
valueOf = placeholderResourceDao.update(newResource).getEntity().getId();
} else {
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
}
ResourceTable target = myIndexingService.getEntityManager().find(ResourceTable.class, valueOf);
RuntimeResourceDefinition targetResourceDef = myIndexingService.getContext().getResourceDefinition(type);
if (target == null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
if (!typeString.equals(target.getResourceType())) {
throw new UnprocessableEntityException(
"Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType());
}
if (target.getDeleted() != null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit);
}
if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) {
continue;
}
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime);
theLinks.add(resourceLink);
}
}
theEntity.setHasLinks(theLinks.size() > 0);
return retVal;
}
private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
nextSearchParam.setUpdated(theUpdateTime);
}
}
private String translatePidIdToForcedId(String theResourceType, Long theId) {
ForcedId forcedId = myIndexingService.getForcedIdDao().findByResourcePid(theId);
if (forcedId != null) {
return forcedId.getResourceType() + '/' + forcedId.getForcedId();
} else {
return theResourceType + '/' + theId.toString();
}
}
public void removeCommon(ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
EntityManager myEntityManager = myIndexingService.getEntityManager();
calculateHashes(stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, stringParams)) {
next.setDaoConfig(myIndexingService.getConfig());
myEntityManager .remove(next);
theEntity.getParamsString().remove(next);
}
for (ResourceIndexedSearchParamString next : removeCommon(stringParams, existingParams.stringParams)) {
myEntityManager.persist(next);
}
calculateHashes(tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, tokenParams)) {
myEntityManager.remove(next);
theEntity.getParamsToken().remove(next);
}
for (ResourceIndexedSearchParamToken next : removeCommon(tokenParams, existingParams.tokenParams)) {
myEntityManager.persist(next);
}
calculateHashes(numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, numberParams)) {
myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next);
}
for (ResourceIndexedSearchParamNumber next : removeCommon(numberParams, existingParams.numberParams)) {
myEntityManager.persist(next);
}
calculateHashes(quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, quantityParams)) {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
}
for (ResourceIndexedSearchParamQuantity next : removeCommon(quantityParams, existingParams.quantityParams)) {
myEntityManager.persist(next);
}
// Store date SP's
calculateHashes(dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, dateParams)) {
myEntityManager.remove(next);
theEntity.getParamsDate().remove(next);
}
for (ResourceIndexedSearchParamDate next : removeCommon(dateParams, existingParams.dateParams)) {
myEntityManager.persist(next);
}
// Store URI SP's
calculateHashes(uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, uriParams)) {
myEntityManager.remove(next);
theEntity.getParamsUri().remove(next);
}
for (ResourceIndexedSearchParamUri next : removeCommon(uriParams, existingParams.uriParams)) {
myEntityManager.persist(next);
}
// Store Coords SP's
calculateHashes(coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, coordsParams)) {
myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next);
}
for (ResourceIndexedSearchParamCoords next : removeCommon(coordsParams, existingParams.coordsParams)) {
myEntityManager.persist(next);
}
// Store resource links
for (ResourceLink next : removeCommon(existingParams.links, links)) {
myEntityManager.remove(next);
theEntity.getResourceLinks().remove(next);
}
for (ResourceLink next : removeCommon(links, existingParams.links)) {
myEntityManager.persist(next);
}
// make sure links are indexed
theEntity.setResourceLinks(links);
// Store composite string uniques
if (myIndexingService.getConfig().isUniqueIndexesEnabled()) {
for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, compositeStringUniques)) {
ourLog.debug("Removing unique index: {}", next);
myEntityManager.remove(next);
theEntity.getParamsCompositeStringUnique().remove(next);
}
for (ResourceIndexedCompositeStringUnique next : removeCommon(compositeStringUniques, existingParams.compositeStringUniques)) {
if (myIndexingService.getConfig().isUniqueIndexesCheckedBeforeSave()) {
ResourceIndexedCompositeStringUnique existing = myIndexingService.getResourceIndexedCompositeStringUniqueDao().findByQueryString(next.getIndexString());
if (existing != null) {
String msg = myIndexingService.getContext().getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new PreconditionFailedException(msg);
}
}
ourLog.debug("Persisting unique index: {}", next);
myEntityManager.persist(next);
}
}
}
private void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
for (BaseResourceIndexedSearchParam next : theStringParams) {
next.calculateHashes();
}
}
private <T> Collection<T> removeCommon(Collection<T> theInput, Collection<T> theToRemove) {
assert theInput != theToRemove;
if (theInput.isEmpty()) {
return theInput;
}
ArrayList<T> retVal = new ArrayList<>(theInput);
retVal.removeAll(theToRemove);
return retVal;
}
public Set<String> getPopulatedResourceLinkParameters() {
return populatedResourceLinkParameters;
}
public boolean matchParam(String theResourceName, String theParamName, RuntimeSearchParam paramDef, IQueryParameterType theParam) {
if (paramDef == null) {
return false;
}
Collection<? extends BaseResourceIndexedSearchParam> resourceParams;
switch (paramDef.getParamType()) {
case TOKEN:
resourceParams = tokenParams;
break;
case QUANTITY:
resourceParams = quantityParams;
break;
case STRING:
resourceParams = stringParams;
break;
case NUMBER:
resourceParams = numberParams;
break;
case URI:
resourceParams = uriParams;
break;
case DATE:
resourceParams = dateParams;
break;
case REFERENCE:
return matchResourceLinks(theResourceName, theParamName, theParam);
case COMPOSITE:
case HAS:
case SPECIAL:
default:
resourceParams = null;
}
if (resourceParams == null) {
return false;
}
Predicate<BaseResourceIndexedSearchParam> namedParamPredicate = param ->
param.getParamName().equalsIgnoreCase(theParamName) &&
param.matches(theParam);
return resourceParams.stream().anyMatch(namedParamPredicate);
}
private boolean matchResourceLinks(String theResourceName, String theParamName, IQueryParameterType theParam) {
ReferenceParam reference = (ReferenceParam)theParam;
Predicate<ResourceLink> namedParamPredicate = resourceLink ->
resourceLinkMatches(theResourceName, resourceLink, theParamName)
&& resourceIdMatches(resourceLink, reference);
return links.stream().anyMatch(namedParamPredicate);
}
private boolean resourceIdMatches(ResourceLink theResourceLink, ReferenceParam theReference) {
ResourceTable target = theResourceLink.getTargetResource();
IdDt idDt = target.getIdDt();
if (idDt.isIdPartValidLong()) {
return theReference.getIdPartAsLong() == idDt.getIdPartAsLong();
} else {
ForcedId forcedId = target.getForcedId();
if (forcedId != null) {
return forcedId.getForcedId().equals(theReference.getValue());
} else {
return false;
}
}
}
private boolean resourceLinkMatches(String theResourceName, ResourceLink theResourceLink, String theParamName) {
return theResourceLink.getTargetResource().getResourceType().equalsIgnoreCase(theParamName) ||
theResourceLink.getSourcePath().equalsIgnoreCase(theResourceName+"."+theParamName);
}
@Override
public String toString() {
return "ResourceIndexedSearchParams{" +
"stringParams=" + stringParams +
", tokenParams=" + tokenParams +
", numberParams=" + numberParams +
", quantityParams=" + quantityParams +
", dateParams=" + dateParams +
", uriParams=" + uriParams +
", coordsParams=" + coordsParams +
", compositeStringUniques=" + compositeStringUniques +
", links=" + links +
'}';
}
void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set<Entry<String, RuntimeSearchParam>> theActiveSearchParams) {
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.STRING, stringParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.NUMBER, numberParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.QUANTITY, quantityParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.DATE, dateParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.URI, uriParams);
findMissingSearchParams(theDaoConfig, theEntity, theActiveSearchParams, RestSearchParameterTypeEnum.TOKEN, tokenParams);
}
@SuppressWarnings("unchecked")
private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(DaoConfig theDaoConfig, ResourceTable theEntity, Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams, RestSearchParameterTypeEnum type,
Collection<RT> paramCollection) {
for (Map.Entry<String, RuntimeSearchParam> nextEntry : activeSearchParams) {
String nextParamName = nextEntry.getKey();
if (nextEntry.getValue().getParamType() == type) {
boolean haveParam = false;
for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
if (nextParam.getParamName().equals(nextParamName)) {
haveParam = true;
break;
}
}
if (!haveParam) {
BaseResourceIndexedSearchParam param;
switch (type) {
case DATE:
param = new ResourceIndexedSearchParamDate();
break;
case NUMBER:
param = new ResourceIndexedSearchParamNumber();
break;
case QUANTITY:
param = new ResourceIndexedSearchParamQuantity();
break;
case STRING:
param = new ResourceIndexedSearchParamString()
.setDaoConfig(theDaoConfig);
break;
case TOKEN:
param = new ResourceIndexedSearchParamToken();
break;
case URI:
param = new ResourceIndexedSearchParamUri();
break;
case COMPOSITE:
case HAS:
case REFERENCE:
default:
continue;
}
param.setResource(theEntity);
param.setMissing(true);
param.setParamName(nextParamName);
paramCollection.add((RT) param);
}
}
}
}
}

View File

@ -0,0 +1,587 @@
package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.data.IResourceIndexedCompositeStringUniqueDao;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
@Lazy
public class SearchParamExtractorService {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class);
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private FhirContext myContext;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@Autowired
private ISearchParamExtractor mySearchParamExtractor;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
public void populateFromResource(ResourceIndexedSearchParams theParams, IDao theCallingDao, Date theUpdateTime, ResourceTable theEntity, IBaseResource theResource, ResourceIndexedSearchParams existingParams) {
extractFromResource(theParams, theEntity, theResource);
Set<Map.Entry<String, RuntimeSearchParam>> activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType()).entrySet();
if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) {
theParams.findMissingSearchParams(myDaoConfig, theEntity, activeSearchParams);
}
theParams.setUpdatedTime(theUpdateTime);
extractInlineReferences(theResource);
extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, true);
/*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
*/
for (Iterator<ResourceLink> existingLinkIter = existingParams.getResourceLinks().iterator(); existingLinkIter.hasNext(); ) {
ResourceLink nextExisting = existingLinkIter.next();
if (theParams.links.remove(nextExisting)) {
existingLinkIter.remove();
theParams.links.add(nextExisting);
}
}
/*
* Handle composites
*/
extractCompositeStringUniques(theEntity, theParams);
}
public void extractFromResource(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource) {
theParams.stringParams.addAll(extractSearchParamStrings(theEntity, theResource));
theParams.numberParams.addAll(extractSearchParamNumber(theEntity, theResource));
theParams.quantityParams.addAll(extractSearchParamQuantity(theEntity, theResource));
theParams.dateParams.addAll(extractSearchParamDates(theEntity, theResource));
theParams.uriParams.addAll(extractSearchParamUri(theEntity, theResource));
theParams.coordsParams.addAll(extractSearchParamCoords(theEntity, theResource));
ourLog.trace("Storing date indexes: {}", theParams.dateParams);
for (BaseResourceIndexedSearchParam next : extractSearchParamTokens(theEntity, theResource)) {
if (next instanceof ResourceIndexedSearchParamToken) {
theParams.tokenParams.add((ResourceIndexedSearchParamToken) next);
} else {
theParams.stringParams.add((ResourceIndexedSearchParamString) next);
}
}
}
/**
* Handle references within the resource that are match URLs, for example references like "Patient?identifier=foo". These match URLs are resolved and replaced with the ID of the
* matching resource.
*/
public void extractInlineReferences(IBaseResource theResource) {
if (!myDaoConfig.isAllowInlineMatchUrlReferences()) {
return;
}
FhirTerser terser = myContext.newTerser();
List<IBaseReference> allRefs = terser.getAllPopulatedChildElementsOfType(theResource, IBaseReference.class);
for (IBaseReference nextRef : allRefs) {
IIdType nextId = nextRef.getReferenceElement();
String nextIdText = nextId.getValue();
if (nextIdText == null) {
continue;
}
int qmIndex = nextIdText.indexOf('?');
if (qmIndex != -1) {
for (int i = qmIndex - 1; i >= 0; i--) {
if (nextIdText.charAt(i) == '/') {
if (i < nextIdText.length() - 1 && nextIdText.charAt(i + 1) == '?') {
// Just in case the URL is in the form Patient/?foo=bar
continue;
}
nextIdText = nextIdText.substring(i + 1);
break;
}
}
String resourceTypeString = nextIdText.substring(0, nextIdText.indexOf('?')).replace("/", "");
RuntimeResourceDefinition matchResourceDef = myContext.getResourceDefinition(resourceTypeString);
if (matchResourceDef == null) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlInvalidResourceType", nextId.getValue(), resourceTypeString);
throw new InvalidRequestException(msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
Set<Long> matches = myMatchUrlService.processMatchUrl(nextIdText, matchResourceType);
if (matches.isEmpty()) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", nextId.getValue());
throw new ResourceNotFoundException(msg);
}
if (matches.size() > 1) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "invalidMatchUrlMultipleMatches", nextId.getValue());
throw new PreconditionFailedException(msg);
}
Long next = matches.iterator().next();
String newId = myIdHelperService.translatePidIdToForcedId(resourceTypeString, next);
ourLog.debug("Replacing inline match URL[{}] with ID[{}}", nextId.getValue(), newId);
nextRef.setReference(newId);
}
}
}
protected Set<ResourceIndexedSearchParamCoords> extractSearchParamCoords(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamCoords(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamDate> extractSearchParamDates(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamDates(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamNumber> extractSearchParamNumber(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamNumber(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamQuantity(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamString> extractSearchParamStrings(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamStrings(theEntity, theResource);
}
protected Set<BaseResourceIndexedSearchParam> extractSearchParamTokens(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamTokens(theEntity, theResource);
}
protected Set<ResourceIndexedSearchParamUri> extractSearchParamUri(ResourceTable theEntity, IBaseResource theResource) {
return mySearchParamExtractor.extractSearchParamUri(theEntity, theResource);
}
private void extractCompositeStringUniques(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
List<JpaRuntimeSearchParam> uniqueSearchParams = mySearchParamRegistry.getActiveUniqueSearchParams(theEntity.getResourceType());
for (JpaRuntimeSearchParam next : uniqueSearchParams) {
List<List<String>> partsChoices = new ArrayList<>();
for (RuntimeSearchParam nextCompositeOf : next.getCompositeOf()) {
Collection<? extends BaseResourceIndexedSearchParam> paramsListForCompositePart = null;
Collection<ResourceLink> linksForCompositePart = null;
Collection<String> linksForCompositePartWantPaths = null;
switch (nextCompositeOf.getParamType()) {
case NUMBER:
paramsListForCompositePart = theParams.numberParams;
break;
case DATE:
paramsListForCompositePart = theParams.dateParams;
break;
case STRING:
paramsListForCompositePart = theParams.stringParams;
break;
case TOKEN:
paramsListForCompositePart = theParams.tokenParams;
break;
case REFERENCE:
linksForCompositePart = theParams.links;
linksForCompositePartWantPaths = new HashSet<>();
linksForCompositePartWantPaths.addAll(nextCompositeOf.getPathsSplit());
break;
case QUANTITY:
paramsListForCompositePart = theParams.quantityParams;
break;
case URI:
paramsListForCompositePart = theParams.uriParams;
break;
case COMPOSITE:
case HAS:
break;
}
ArrayList<String> nextChoicesList = new ArrayList<>();
partsChoices.add(nextChoicesList);
String key = UrlUtil.escapeUrlParam(nextCompositeOf.getName());
if (paramsListForCompositePart != null) {
for (BaseResourceIndexedSearchParam nextParam : paramsListForCompositePart) {
if (nextParam.getParamName().equals(nextCompositeOf.getName())) {
IQueryParameterType nextParamAsClientParam = nextParam.toQueryParameterType();
String value = nextParamAsClientParam.getValueAsQueryToken(myContext);
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
if (linksForCompositePart != null) {
for (ResourceLink nextLink : linksForCompositePart) {
if (linksForCompositePartWantPaths.contains(nextLink.getSourcePath())) {
String value = nextLink.getTargetResource().getIdDt().toUnqualifiedVersionless().getValue();
if (isNotBlank(value)) {
value = UrlUtil.escapeUrlParam(value);
nextChoicesList.add(key + "=" + value);
}
}
}
}
}
Set<String> queryStringsToPopulate = theParams.extractCompositeStringUniquesValueChains(theEntity.getResourceType(), partsChoices);
for (String nextQueryString : queryStringsToPopulate) {
if (isNotBlank(nextQueryString)) {
theParams.compositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
}
}
}
}
/**
* @return Returns a set containing all of the parameter names that
* were found to have a value
*/
@SuppressWarnings("unchecked")
public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, boolean lookUpReferencesInDatabase) {
String resourceType = theEntity.getResourceType();
/*
* For now we don't try to load any of the links in a bundle if it's the actual bundle we're storing..
*/
if (theResource instanceof IBaseBundle) {
return;
}
Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass()));
for (RuntimeSearchParam nextSpDef : searchParams.values()) {
if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
continue;
}
String nextPathsUnsplit = nextSpDef.getPath();
if (isBlank(nextPathsUnsplit)) {
continue;
}
boolean multiType = false;
if (nextPathsUnsplit.endsWith("[x]")) {
multiType = true;
}
List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef);
for (PathAndRef nextPathAndRef : refs) {
Object nextObject = nextPathAndRef.getRef();
/*
* A search parameter on an extension field that contains
* references should index those references
*/
if (nextObject instanceof IBaseExtension<?, ?>) {
nextObject = ((IBaseExtension<?, ?>) nextObject).getValue();
}
if (nextObject instanceof CanonicalType) {
nextObject = new Reference(((CanonicalType) nextObject).getValueAsString());
}
IIdType nextId;
if (nextObject instanceof IBaseReference) {
IBaseReference nextValue = (IBaseReference) nextObject;
if (nextValue.isEmpty()) {
continue;
}
nextId = nextValue.getReferenceElement();
/*
* This can only really happen if the DAO is being called
* programatically with a Bundle (not through the FHIR REST API)
* but Smile does this
*/
if (nextId.isEmpty() && nextValue.getResource() != null) {
nextId = nextValue.getResource().getIdElement();
}
if (nextId.isEmpty() || nextId.getValue().startsWith("#")) {
// This is a blank or contained resource reference
continue;
}
} else if (nextObject instanceof IBaseResource) {
nextId = ((IBaseResource) nextObject).getIdElement();
if (nextId == null || nextId.hasIdPart() == false) {
continue;
}
} else if (myContext.getElementDefinition((Class<? extends IBase>) nextObject.getClass()).getName().equals("uri")) {
continue;
} else if (resourceType.equals("Consent") && nextPathAndRef.getPath().equals("Consent.source")) {
// Consent#source-identifier has a path that isn't typed - This is a one-off to deal with that
continue;
} else {
if (!multiType) {
if (nextSpDef.getName().equals("sourceuri")) {
continue; // TODO: disable this eventually - ConceptMap:sourceuri is of type reference but points to a URI
}
throw new ConfigurationException("Search param " + nextSpDef.getName() + " is of unexpected datatype: " + nextObject.getClass());
} else {
continue;
}
}
theParams.populatedResourceLinkParameters.add(nextSpDef.getName());
if (LogicalReferenceHelper.isLogicalReference(myDaoConfig, nextId)) {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theParams.links.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
String baseUrl = nextId.getBaseUrl();
String typeString = nextId.getResourceType();
if (isBlank(typeString)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue());
}
RuntimeResourceDefinition resourceDefinition;
try {
resourceDefinition = myContext.getResourceDefinition(typeString);
} catch (DataFormatException e) {
throw new InvalidRequestException(
"Invalid resource reference found at path[" + nextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue());
}
if (isNotBlank(baseUrl)) {
if (!myDaoConfig.getTreatBaseUrlsAsLocal().contains(baseUrl) && !myDaoConfig.isAllowExternalReferences()) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "externalReferenceNotAllowed", nextId.getValue());
throw new InvalidRequestException(msg);
} else {
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, nextId, theUpdateTime);
if (theParams.links.add(resourceLink)) {
ourLog.debug("Indexing remote resource reference URL: {}", nextId);
}
continue;
}
}
Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
String id = nextId.getIdPart();
if (StringUtils.isBlank(id)) {
throw new InvalidRequestException("Invalid resource reference found at path[" + nextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue());
}
myDaoRegistry.getDaoOrThrowException(type);
ResourceTable target;
if (lookUpReferencesInDatabase) {
Long valueOf;
try {
valueOf = myIdHelperService.translateForcedIdToPid(typeString, id);
} catch (ResourceNotFoundException e) {
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
continue;
}
RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type);
String resName = missingResourceDef.getName();
if (myDaoConfig.isAutoCreatePlaceholderReferenceTargets()) {
IBaseResource newResource = missingResourceDef.newInstance();
newResource.setId(resName + "/" + id);
IFhirResourceDao<IBaseResource> placeholderResourceDao = (IFhirResourceDao<IBaseResource>) myDaoRegistry.getResourceDao(newResource.getClass());
ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
valueOf = placeholderResourceDao.update(newResource).getEntity().getId();
} else {
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
}
target = myEntityManager.find(ResourceTable.class, valueOf);
RuntimeResourceDefinition targetResourceDef = myContext.getResourceDefinition(type);
if (target == null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " not found, specified in path: " + nextPathsUnsplit);
}
if (!typeString.equals(target.getResourceType())) {
throw new UnprocessableEntityException(
"Resource contains reference to " + nextId.getValue() + " but resource with ID " + nextId.getIdPart() + " is actually of type " + target.getResourceType());
}
if (target.getDeleted() != null) {
String resName = targetResourceDef.getName();
throw new InvalidRequestException("Resource " + resName + "/" + id + " is deleted, specified in path: " + nextPathsUnsplit);
}
if (nextSpDef.getTargets() != null && !nextSpDef.getTargets().contains(typeString)) {
continue;
}
} else {
target = new ResourceTable();
target.setResourceType(typeString);
if (nextId.isIdPartValidLong()) {
target.setId(nextId.getIdPartAsLong());
} else {
ForcedId forcedId = new ForcedId();
forcedId.setForcedId(id);
target.setForcedId(forcedId);
}
}
ResourceLink resourceLink = new ResourceLink(nextPathAndRef.getPath(), theEntity, target, theUpdateTime);
theParams.links.add(resourceLink);
}
}
theEntity.setHasLinks(theParams.links.size() > 0);
}
public String toResourceName(Class<? extends IBaseResource> theResourceType) {
return myContext.getResourceDefinition(theResourceType).getName();
}
public void removeCommon(ResourceIndexedSearchParams theParams, ResourceTable theEntity, ResourceIndexedSearchParams existingParams) {
theParams.calculateHashes(theParams.stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingParams.stringParams, theParams.stringParams)) {
next.setDaoConfig(myDaoConfig);
myEntityManager.remove(next);
theEntity.getParamsString().remove(next);
}
for (ResourceIndexedSearchParamString next : removeCommon(theParams.stringParams, existingParams.stringParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingParams.tokenParams, theParams.tokenParams)) {
myEntityManager.remove(next);
theEntity.getParamsToken().remove(next);
}
for (ResourceIndexedSearchParamToken next : removeCommon(theParams.tokenParams, existingParams.tokenParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingParams.numberParams, theParams.numberParams)) {
myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next);
}
for (ResourceIndexedSearchParamNumber next : removeCommon(theParams.numberParams, existingParams.numberParams)) {
myEntityManager.persist(next);
}
theParams.calculateHashes(theParams.quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingParams.quantityParams, theParams.quantityParams)) {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
}
for (ResourceIndexedSearchParamQuantity next : removeCommon(theParams.quantityParams, existingParams.quantityParams)) {
myEntityManager.persist(next);
}
// Store date SP's
theParams.calculateHashes(theParams.dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingParams.dateParams, theParams.dateParams)) {
myEntityManager.remove(next);
theEntity.getParamsDate().remove(next);
}
for (ResourceIndexedSearchParamDate next : removeCommon(theParams.dateParams, existingParams.dateParams)) {
myEntityManager.persist(next);
}
// Store URI SP's
theParams.calculateHashes(theParams.uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingParams.uriParams, theParams.uriParams)) {
myEntityManager.remove(next);
theEntity.getParamsUri().remove(next);
}
for (ResourceIndexedSearchParamUri next : removeCommon(theParams.uriParams, existingParams.uriParams)) {
myEntityManager.persist(next);
}
// Store Coords SP's
theParams.calculateHashes(theParams.coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingParams.coordsParams, theParams.coordsParams)) {
myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next);
}
for (ResourceIndexedSearchParamCoords next : removeCommon(theParams.coordsParams, existingParams.coordsParams)) {
myEntityManager.persist(next);
}
// Store resource links
for (ResourceLink next : removeCommon(existingParams.links, theParams.links)) {
myEntityManager.remove(next);
theEntity.getResourceLinks().remove(next);
}
for (ResourceLink next : removeCommon(theParams.links, existingParams.links)) {
myEntityManager.persist(next);
}
// make sure links are indexed
theEntity.setResourceLinks(theParams.links);
// Store composite string uniques
if (myDaoConfig.isUniqueIndexesEnabled()) {
for (ResourceIndexedCompositeStringUnique next : removeCommon(existingParams.compositeStringUniques, theParams.compositeStringUniques)) {
ourLog.debug("Removing unique index: {}", next);
myEntityManager.remove(next);
theEntity.getParamsCompositeStringUnique().remove(next);
}
for (ResourceIndexedCompositeStringUnique next : removeCommon(theParams.compositeStringUniques, existingParams.compositeStringUniques)) {
if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) {
ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
if (existing != null) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "uniqueIndexConflictFailure", theEntity.getResourceType(), next.getIndexString(), existing.getResource().getIdDt().toUnqualifiedVersionless().getValue());
throw new PreconditionFailedException(msg);
}
}
ourLog.debug("Persisting unique index: {}", next);
myEntityManager.persist(next);
}
}
}
private <T> Collection<T> removeCommon(Collection<T> theInput, Collection<T> theToRemove) {
assert theInput != theToRemove;
if (theInput.isEmpty()) {
return theInput;
}
ArrayList<T> retVal = new ArrayList<>(theInput);
retVal.removeAll(theToRemove);
return retVal;
}
}

View File

@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.LogicUtil;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenParam;
@ -64,8 +63,6 @@ public class FhirResourceDaoCodeSystemR4 extends FhirResourceDaoR4<CodeSystem> i
@Autowired
private ITermCodeSystemDao myCsDao;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private ValidationSupportChain myValidationSupport;
@Override

View File

@ -156,4 +156,7 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable {
return hashCode.asLong();
}
public boolean matches(IQueryParameterType theParam) {
throw new UnsupportedOperationException("No parameter matcher for "+theParam);
}
}

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -184,4 +185,31 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
b.append("valueHigh", new InstantDt(getValueHigh()));
return b.build();
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof DateParam)) {
return false;
}
DateParam date = (DateParam) theParam;
DateRangeParam range = new DateRangeParam(date);
Date lowerBound = range.getLowerBoundAsInstant();
Date upperBound = range.getUpperBoundAsInstant();
if (lowerBound == null && upperBound == null) {
// should never happen
return false;
}
boolean result = true;
if (lowerBound != null) {
result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
result &= (myValueHigh.after(lowerBound) || myValueHigh.equals(lowerBound));
}
if (upperBound != null) {
result &= (myValueLow.before(upperBound) || myValueLow.equals(upperBound));
result &= (myValueHigh.before(upperBound) || myValueHigh.equals(upperBound));
}
return result;
}
}

View File

@ -149,4 +149,13 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP
b.append("value", getValue());
return b.build();
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof NumberParam)) {
return false;
}
NumberParam number = (NumberParam)theParam;
return getValue().equals(number.getValue());
}
}

View File

@ -235,4 +235,37 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc
return hash(theResourceType, theParamName, theUnits);
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof QuantityParam)) {
return false;
}
QuantityParam quantity = (QuantityParam)theParam;
boolean retval = false;
// Only match on system if it wasn't specified
if (quantity.getSystem() == null && quantity.getUnits() == null) {
if (getValue().equals(quantity.getValue())) {
retval = true;
}
} else if (quantity.getSystem() == null) {
if (getUnits().equalsIgnoreCase(quantity.getUnits()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
} else if (quantity.getUnits() == null) {
if (getSystem().equalsIgnoreCase(quantity.getSystem()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
} else {
if (getSystem().equalsIgnoreCase(quantity.getSystem()) &&
getUnits().equalsIgnoreCase(quantity.getUnits()) &&
getValue().equals(quantity.getValue())) {
retval = true;
}
}
return retval;
}
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.entity;
* #L%
*/
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.StringParam;
@ -294,4 +295,13 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
return hash(theResourceType, theParamName, left(theValueNormalized, hashPrefixLength));
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof StringParam)) {
return false;
}
StringParam string = (StringParam)theParam;
String normalizedString = BaseHapiFhirDao.normalizeString(string.getValue());
return getValueNormalized().startsWith(normalizedString);
}
}

View File

@ -251,4 +251,29 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa
public static long calculateHashValue(String theResourceType, String theParamName, String theValue) {
return hash(theResourceType, theParamName, trim(theValue));
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof TokenParam)) {
return false;
}
TokenParam token = (TokenParam)theParam;
boolean retval = false;
// Only match on system if it wasn't specified
if (token.getSystem() == null || token.getSystem().isEmpty()) {
if (getValue().equalsIgnoreCase(token.getValue())) {
retval = true;
}
} else if (token.getValue() == null || token.getValue().isEmpty()) {
if (token.getSystem().equalsIgnoreCase(getSystem())) {
retval = true;
}
} else {
if (token.getSystem().equalsIgnoreCase(getSystem()) &&
getValue().equalsIgnoreCase(token.getValue())) {
retval = true;
}
}
return retval;
}
}

View File

@ -179,4 +179,13 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
return hash(theResourceType, theParamName, theUri);
}
@Override
public boolean matches(IQueryParameterType theParam) {
if (!(theParam instanceof UriParam)) {
return false;
}
UriParam uri = (UriParam)theParam;
return getUri().equalsIgnoreCase(uri.getValueNotNull());
}
}

View File

@ -64,7 +64,7 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
for (Argument nextArgument : theSearchParams) {
RuntimeSearchParam searchParam = getSearchParamByName(typeDef, nextArgument.getName());
RuntimeSearchParam searchParam = mySearchParamRegistry.getSearchParamByName(typeDef, nextArgument.getName());
for (Value nextValue : nextArgument.getValues()) {
String value = nextValue.getValue();

View File

@ -22,12 +22,12 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.util.JpaConstants;
@ -86,6 +86,8 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
@Autowired
private ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired
private MatchUrlService myMatchUrlService;
private ApplicationContext myAppCtx;
private ExecutorService myExecutorService;
@ -283,7 +285,7 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
String resourceType = resourceDef.getName();
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceType);
SearchParameterMap params = BaseHapiFhirDao.translateMatchUrl(callingDao, myFhirContext, queryPart, resourceDef);
SearchParameterMap params = myMatchUrlService.translateMatchUrl(queryPart, resourceDef);
ourLog.info("Triggering job[{}] is starting a search for {}", theJobDetails.getJobId(), nextSearchUrl);

View File

@ -23,6 +23,7 @@ import java.util.*;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CapabilityStatement.*;
import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType;
@ -41,6 +42,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
private volatile CapabilityStatement myCachedValue;
private DaoConfig myDaoConfig;
private ISearchParamRegistry mySearchParamRegistry;
private String myImplementationDescription;
private boolean myIncludeResourceCounts;
private RestfulServer myRestfulServer;
@ -64,6 +66,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
myRestfulServer = theRestfulServer;
mySystemDao = theSystemDao;
myDaoConfig = theDaoConfig;
mySearchParamRegistry = theSystemDao.getSearchParamRegistry();
super.setCache(false);
setIncludeResourceCounts(true);
}
@ -99,7 +102,7 @@ public class JpaConformanceProviderDstu3 extends org.hl7.fhir.dstu3.hapi.rest.se
nextResource.getSearchParam().clear();
String resourceName = nextResource.getType();
RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
Collection<RuntimeSearchParam> searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef);
Collection<RuntimeSearchParam> searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef);
for (RuntimeSearchParam runtimeSp : searchParams) {
CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam();

View File

@ -23,6 +23,7 @@ import java.util.*;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.CapabilityStatement.*;
import org.hl7.fhir.r4.model.Enumerations.SearchParamType;
@ -41,6 +42,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
private volatile CapabilityStatement myCachedValue;
private DaoConfig myDaoConfig;
private ISearchParamRegistry mySearchParamRegistry;
private String myImplementationDescription;
private boolean myIncludeResourceCounts;
private RestfulServer myRestfulServer;
@ -64,6 +66,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
myRestfulServer = theRestfulServer;
mySystemDao = theSystemDao;
myDaoConfig = theDaoConfig;
mySearchParamRegistry = theSystemDao.getSearchParamRegistry();
super.setCache(false);
setIncludeResourceCounts(true);
}
@ -99,7 +102,7 @@ public class JpaConformanceProviderR4 extends org.hl7.fhir.r4.hapi.rest.server.S
nextResource.getSearchParam().clear();
String resourceName = nextResource.getType();
RuntimeResourceDefinition resourceDef = myRestfulServer.getFhirContext().getResourceDefinition(resourceName);
Collection<RuntimeSearchParam> searchParams = mySystemDao.getSearchParamsByResourceType(resourceDef);
Collection<RuntimeSearchParam> searchParams = mySearchParamRegistry.getSearchParamsByResourceType(resourceDef);
for (RuntimeSearchParam runtimeSp : searchParams) {
CapabilityStatementRestResourceSearchParamComponent confSp = nextResource.addSearchParam();

View File

@ -26,7 +26,9 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.BasePagingProvider;
import ca.uhn.fhir.rest.server.IPagingProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DatabaseBackedPagingProvider extends BasePagingProvider implements IPagingProvider {
@Autowired

View File

@ -276,8 +276,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
protected List<IBaseResource> toResourceList(ISearchBuilder sb, List<Long> pidsSubList) {
Set<Long> includedPids = new HashSet<>();
if (mySearchEntity.getSearchType() == SearchTypeEnum.SEARCH) {
includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myDao, myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toRevIncludesList(), true, mySearchEntity.getLastUpdated(), myUuid));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pidsSubList, mySearchEntity.toIncludesList(), false, mySearchEntity.getLastUpdated(), myUuid));
}
// Execute the query and make sure we return distinct results

View File

@ -47,11 +47,15 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.data.domain.AbstractPageRequest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@ -68,6 +72,7 @@ import java.util.concurrent.*;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@Component("mySearchCoordinatorSvc")
public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public static final int DEFAULT_SYNC_SIZE = 250;
@ -313,8 +318,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* individually for pages as we return them to clients
*/
final Set<Long> includedPids = new HashSet<>();
includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(theCallingDao, myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getRevIncludes(), true, theParams.getLastUpdated(), "(synchronous)"));
includedPids.addAll(sb.loadIncludes(myContext, myEntityManager, pids, theParams.getIncludes(), false, theParams.getLastUpdated(), "(synchronous)"));
List<IBaseResource> resources = new ArrayList<>();
sb.loadResourcesByPid(pids, resources, includedPids, false, myEntityManager, myContext, theCallingDao);

View File

@ -31,6 +31,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -43,6 +44,7 @@ import java.util.Date;
/**
* Deletes old searches
*/
@Service
public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
public static final long DEFAULT_CUTOFF_SLACK = 10 * DateUtils.MILLIS_PER_SECOND;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StaleSearchDeletingSvcImpl.class);

View File

@ -458,7 +458,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc {
* not get this error, so we'll let the other one fail and try
* again later.
*/
ourLog.info("Failed to reindex {} because of a version conflict. Leaving in unindexed state: {}", e.getMessage());
ourLog.info("Failed to reindex because of a version conflict. Leaving in unindexed state: {}", e.getMessage());
reindexFailure = null;
}

View File

@ -23,10 +23,15 @@ package ca.uhn.fhir.jpa.search.warm;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.parser.DataFormatException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
@ -34,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
@Autowired
@ -43,6 +49,8 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
private FhirContext myCtx;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MatchUrlService myMatchUrlService;
@Override
@Scheduled(fixedDelay = 1000)
@ -72,7 +80,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc {
RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl);
IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName());
String queryPart = parseWarmUrlParamPart(nextUrl);
SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(callingDao, myCtx, queryPart, resourceDef);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef);
callingDao.search(responseCriteriaUrl);
}

View File

@ -25,10 +25,12 @@ import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.SearchParamPresent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.Map.Entry;
@Service
public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc {
@Autowired

View File

@ -20,21 +20,19 @@ package ca.uhn.fhir.jpa.subscription;
* #L%
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;
public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptionSubscriber {
private static final Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionDeliverySubscriber.class);
public BaseSubscriptionDeliverySubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor);
public BaseSubscriptionDeliverySubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theChannelType, theSubscriptionInterceptor);
}
@Override
@ -59,21 +57,6 @@ public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptio
return;
}
// Load the resource
IIdType payloadId = msg.getPayloadId(getContext());
Class type = getContext().getResourceDefinition(payloadId.getResourceType()).getImplementingClass();
IFhirResourceDao dao = getSubscriptionInterceptor().getDao(type);
IBaseResource loadedPayload;
try {
loadedPayload = dao.read(payloadId);
} catch (ResourceNotFoundException e) {
// This can happen if a last minute failure happens when saving a resource,
// eg a constraint causes the transaction to roll back on commit
ourLog.warn("Unable to find resource {} - Aborting delivery", payloadId.getValue());
return;
}
msg.setPayload(getContext(), loadedPayload);
handleMessage(msg);
} catch (Exception e) {
String msg = "Failure handling subscription payload for subscription: " + subscriptionId;

View File

@ -1,42 +1,27 @@
package ca.uhn.fhir.jpa.subscription;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherCompositeInMemoryDatabase;
import ca.uhn.fhir.jpa.subscription.matcher.SubscriptionMatcherDatabase;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
@ -52,6 +37,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -73,6 +59,26 @@ import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> extends ServerOperationInterceptorAdapter {
static final String SUBSCRIPTION_STATUS = "Subscription.status";
@ -91,9 +97,7 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class);
private ThreadPoolExecutor myDeliveryExecutor;
private LinkedBlockingQueue<Runnable> myProcessingExecutorQueue;
private IFhirResourceDao<?> mySubscriptionDao;
@Autowired
private List<IFhirResourceDao<?>> myResourceDaos;
@Autowired
private FhirContext myCtx;
@Autowired(required = false)
@ -104,7 +108,16 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
@Autowired
@Qualifier(BaseConfig.TASK_EXECUTOR_NAME)
private AsyncTaskExecutor myAsyncTaskExecutor;
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
@Autowired
private SubscriptionMatcherCompositeInMemoryDatabase mySubscriptionMatcherCompositeInMemoryDatabase;
@Autowired
private SubscriptionMatcherDatabase mySubscriptionMatcherDatabase;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private BeanFactory beanFactory;
@Autowired
private MatchUrlService myMatchUrlService;
private Semaphore myInitSubscriptionsSemaphore = new Semaphore(1);
/**
@ -286,26 +299,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
public abstract Subscription.SubscriptionChannelType getChannelType();
// TODO KHS move out
@SuppressWarnings("unchecked")
public <R extends IBaseResource> IFhirResourceDao<R> getDao(Class<R> theType) {
if (myResourceTypeToDao == null) {
Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> theResourceTypeToDao = new HashMap<>();
for (IFhirResourceDao<?> next : myResourceDaos) {
theResourceTypeToDao.put(next.getResourceType(), next);
}
if (this instanceof IFhirResourceDao<?>) {
IFhirResourceDao<?> thiz = (IFhirResourceDao<?>) this;
theResourceTypeToDao.put(thiz.getResourceType(), thiz);
}
myResourceTypeToDao = theResourceTypeToDao;
}
return (IFhirResourceDao<R>) myResourceTypeToDao.get(theType);
}
protected MessageChannel getDeliveryChannel(CanonicalSubscription theSubscription) {
return mySubscribableChannel.get(theSubscription.getIdElement(myCtx).getIdPart());
}
@ -335,9 +328,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
myProcessingChannel = theProcessingChannel;
}
protected IFhirResourceDao<?> getSubscriptionDao() {
return mySubscriptionDao;
}
public List<CanonicalSubscription> getRegisteredSubscriptions() {
return new ArrayList<>(myIdToSubscription.values());
@ -378,7 +368,8 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
IBundleProvider subscriptionBundleList = getSubscriptionDao().search(map, req);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
IBundleProvider subscriptionBundleList = subscriptionDao.search(map, req);
if (subscriptionBundleList.size() >= MAX_SUBSCRIPTION_RESULTS) {
ourLog.error("Currently over " + MAX_SUBSCRIPTION_RESULTS + " subscriptions. Some subscriptions have not been loaded.");
}
@ -436,8 +427,7 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
protected void registerSubscriptionCheckingSubscriber() {
if (mySubscriptionCheckingSubscriber == null) {
ISubscriptionMatcher subscriptionMatcher = new SubscriptionMatcherDatabase(getSubscriptionDao(), this);
mySubscriptionCheckingSubscriber = new SubscriptionCheckingSubscriber(getSubscriptionDao(), getChannelType(), this, subscriptionMatcher );
mySubscriptionCheckingSubscriber = beanFactory.getBean(SubscriptionCheckingSubscriber.class, getChannelType(), this, mySubscriptionMatcherCompositeInMemoryDatabase);
}
getProcessingChannel().subscribe(mySubscriptionCheckingSubscriber);
}
@ -501,9 +491,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
myCtx = theCtx;
}
public void setResourceDaos(List<IFhirResourceDao<?>> theResourceDaos) {
myResourceDaos = theResourceDaos;
}
@VisibleForTesting
public void setTxManager(PlatformTransactionManager theTxManager) {
@ -512,15 +499,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
@PostConstruct
public void start() {
for (IFhirResourceDao<?> next : myResourceDaos) {
if (next.getResourceType() != null) {
if (myCtx.getResourceDefinition(next.getResourceType()).getName().equals("Subscription")) {
mySubscriptionDao = next;
}
}
}
Validate.notNull(mySubscriptionDao);
if (myCtx.getVersion().getVersion() == FhirVersionEnum.R4) {
Validate.notNull(myEventDefinitionDaoR4);
}
@ -555,7 +533,8 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
}
if (mySubscriptionActivatingSubscriber == null) {
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager, myAsyncTaskExecutor);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(subscriptionDao, getChannelType(), this, myTxManager, myAsyncTaskExecutor);
}
registerSubscriptionCheckingSubscriber();
@ -620,4 +599,26 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
}
public IFhirResourceDao<?> getSubscriptionDao() {
return myDaoRegistry.getResourceDao("Subscription");
}
public IFhirResourceDao getDao(Class type) {
return myDaoRegistry.getResourceDao(type);
}
public void setResourceDaos(List<IFhirResourceDao> theResourceDaos) {
myDaoRegistry.setResourceDaos(theResourceDaos);
}
public void validateCriteria(final S theResource) {
CanonicalSubscription subscription = canonicalize(theResource);
String criteria = subscription.getCriteriaString();
try {
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myCtx, criteria);
myMatchUrlService.translateMatchUrl(criteria, resourceDef);
} catch (InvalidRequestException e) {
throw new UnprocessableEntityException("Invalid subscription criteria submitted: "+criteria+" "+e.getMessage());
}
}
}

View File

@ -21,25 +21,39 @@ package ca.uhn.fhir.jpa.subscription;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
public abstract class BaseSubscriptionSubscriber implements MessageHandler {
private final IFhirResourceDao<?> mySubscriptionDao;
@Autowired
DaoRegistry myDaoRegistry;
private final Subscription.SubscriptionChannelType myChannelType;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
private IFhirResourceDao<?> mySubscriptionDao;
/**
* Constructor
*/
public BaseSubscriptionSubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
mySubscriptionDao = theSubscriptionDao;
public BaseSubscriptionSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
myChannelType = theChannelType;
mySubscriptionInterceptor = theSubscriptionInterceptor;
}
@PostConstruct
public void setSubscriptionDao() {
mySubscriptionDao = myDaoRegistry.getResourceDao("Subscription");
}
public Subscription.SubscriptionChannelType getChannelType() {
return myChannelType;
}

View File

@ -162,16 +162,16 @@ public class SubscriptionActivatingSubscriber {
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
public void handleMessage(ResourceModifiedMessage.OperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException {
switch (theOperationType) {
case DELETE:
mySubscriptionInterceptor.unregisterSubscription(theId);
return;
case CREATE:
case UPDATE:
if (!theId.getResourceType().equals("Subscription")) {
return;
}
switch (theOperationType) {
case DELETE:
mySubscriptionInterceptor.unregisterSubscription(theId);
break;
case CREATE:
case UPDATE:
mySubscriptionInterceptor.validateCriteria(theSubscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(theSubscription);
break;
default:

View File

@ -1,16 +1,29 @@
package ca.uhn.fhir.jpa.subscription;
import java.util.List;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*-
* #%L
@ -32,24 +45,18 @@ import org.springframework.messaging.MessagingException;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.subscription.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Component
@Scope("prototype")
public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriber.class);
private final ISubscriptionMatcher mySubscriptionMatcher;
public SubscriptionCheckingSubscriber(IFhirResourceDao theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, ISubscriptionMatcher theSubscriptionMatcher) {
super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor);
@Autowired
private MatchUrlService myMatchUrlService;
public SubscriptionCheckingSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, ISubscriptionMatcher theSubscriptionMatcher) {
super(theChannelType, theSubscriptionInterceptor);
this.mySubscriptionMatcher = theSubscriptionMatcher;
}
@ -77,11 +84,10 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
IIdType id = msg.getId(getContext());
String resourceType = id.getResourceType();
String resourceId = id.getIdPart();
List<CanonicalSubscription> subscriptions = getSubscriptionInterceptor().getRegisteredSubscriptions();
ourLog.trace("Testing {} subscriptions for applicability");
ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size());
for (CanonicalSubscription nextSubscription : subscriptions) {
@ -112,7 +118,7 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
continue;
}
if (!mySubscriptionMatcher.match(nextCriteriaString, msg)) {
if (!mySubscriptionMatcher.match(nextCriteriaString, msg).matched()) {
continue;
}
@ -148,7 +154,7 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
*/
protected IBundleProvider performSearch(String theCriteria) {
RuntimeResourceDefinition responseResourceDef = getSubscriptionDao().validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(getSubscriptionDao(), getSubscriptionDao().getContext(), theCriteria, responseResourceDef);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef);
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);

View File

@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.subscription.email;
* #L%
*/
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage;
@ -28,19 +27,24 @@ import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.*;
@Component
@Scope("prototype")
public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliverySubscriber {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringEmailSubscriber.class);
private SubscriptionEmailInterceptor mySubscriptionEmailInterceptor;
public SubscriptionDeliveringEmailSubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, SubscriptionEmailInterceptor theSubscriptionEmailInterceptor) {
super(theSubscriptionDao, theChannelType, theSubscriptionEmailInterceptor);
public SubscriptionDeliveringEmailSubscriber(Subscription.SubscriptionChannelType theChannelType, SubscriptionEmailInterceptor theSubscriptionEmailInterceptor) {
super(theChannelType, theSubscriptionEmailInterceptor);
mySubscriptionEmailInterceptor = theSubscriptionEmailInterceptor;
}

View File

@ -23,11 +23,22 @@ package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* Note: If you're going to use this, you need to provide a bean
* of type {@link ca.uhn.fhir.jpa.subscription.email.IEmailSender}
* in your own Spring config
*/
@Component
@Lazy
public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
/**
@ -36,11 +47,13 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
*/
@Autowired(required = false)
private IEmailSender myEmailSender;
@Autowired
BeanFactory myBeanFactory;
private String myDefaultFromAddress = "noreply@unknown.com";
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
return Optional.of(new SubscriptionDeliveringEmailSubscriber(getSubscriptionDao(), getChannelType(), this));
return Optional.of(myBeanFactory.getBean(SubscriptionDeliveringEmailSubscriber.class, getChannelType(), this));
}
@Override

View File

@ -0,0 +1,142 @@
package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
@Service
public class CriteriaResourceMatcher {
@Autowired
private FhirContext myContext;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
ISearchParamRegistry mySearchParamRegistry;
public SubscriptionMatchResult match(String theCriteria, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
SearchParameterMap searchParameterMap;
try {
searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, theResourceDefinition);
} catch (UnsupportedOperationException e) {
return new SubscriptionMatchResult(theCriteria);
}
searchParameterMap.clean();
if (searchParameterMap.getLastUpdated() != null) {
return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Qualifiers not supported");
}
for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) {
String theParamName = entry.getKey();
List<List<? extends IQueryParameterType>> theAndOrParams = entry.getValue();
SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theSearchParams);
if (!result.matched()){
return result;
}
}
return SubscriptionMatchResult.MATCH;
}
// This method is modelled from SearchBuilder.searchForIdsWithAndOr()
private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
if (theAndOrParams.isEmpty()) {
return SubscriptionMatchResult.MATCH;
}
if (hasQualifiers(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Qualifiers not supported.");
}
if (hasPrefixes(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Prefixes not supported.");
}
if (hasChain(theAndOrParams)) {
return new SubscriptionMatchResult(theParamName, "Chained references are not supported");
}
if (theParamName.equals(IAnyResource.SP_RES_ID)) {
return new SubscriptionMatchResult(theParamName);
} else if (theParamName.equals(IAnyResource.SP_RES_LANGUAGE)) {
return new SubscriptionMatchResult(theParamName);
} else if (theParamName.equals(Constants.PARAM_HAS)) {
return new SubscriptionMatchResult(theParamName);
} else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) {
return new SubscriptionMatchResult(theParamName);
} else {
String resourceName = theResourceDefinition.getName();
RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName);
return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef);
}
}
private SubscriptionMatchResult matchResourceParam(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) {
if (theParamDef != null) {
switch (theParamDef.getParamType()) {
case QUANTITY:
case TOKEN:
case STRING:
case NUMBER:
case URI:
case DATE:
case REFERENCE:
return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
case COMPOSITE:
case HAS:
case SPECIAL:
default:
return new SubscriptionMatchResult(theParamName);
}
} else {
if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
return new SubscriptionMatchResult(theParamName);
} else {
throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
}
}
}
private boolean matchParams(String theResourceName, String theParamName, RuntimeSearchParam paramDef, List<? extends IQueryParameterType> theNextAnd, ResourceIndexedSearchParams theSearchParams) {
return theNextAnd.stream().anyMatch(token -> theSearchParams.matchParam(theResourceName, theParamName, paramDef, token));
}
private boolean hasChain(List<List<? extends IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param instanceof ReferenceParam && ((ReferenceParam)param).getChain() != null);
}
private boolean hasQualifiers(List<List<? extends IQueryParameterType>> theAndOrParams) {
return theAndOrParams.stream().flatMap(List::stream).anyMatch(param -> param.getQueryParameterQualifier() != null);
}
private boolean hasPrefixes(List<List<? extends IQueryParameterType>> theAndOrParams) {
Predicate<IQueryParameterType> hasPrefixPredicate = param -> param instanceof BaseParamWithPrefix &&
((BaseParamWithPrefix) param).getPrefix() != null;
return theAndOrParams.stream().flatMap(List::stream).anyMatch(hasPrefixPredicate);
}
}

View File

@ -23,5 +23,5 @@ package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
public interface ISubscriptionMatcher {
boolean match(String criteria, ResourceModifiedMessage msg);
SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg);
}

View File

@ -0,0 +1,7 @@
package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
public interface ISubscriptionMatcher {
boolean match(String criteria, ResourceModifiedMessage msg);
}

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.jpa.subscription.matcher;
public class SubscriptionMatchResult {
// This could be an enum, but we may want to include details about unsupported matches in the future
public static final SubscriptionMatchResult MATCH = new SubscriptionMatchResult(true);
public static final SubscriptionMatchResult NO_MATCH = new SubscriptionMatchResult(false);
private final boolean myMatch;
private final boolean mySupported;
private final String myUnsupportedParameter;
private final String myUnsupportedReason;
public SubscriptionMatchResult(boolean theMatch) {
this.myMatch = theMatch;
this.mySupported = true;
this.myUnsupportedParameter = null;
this.myUnsupportedReason = null;
}
public SubscriptionMatchResult(String theUnsupportedParameter) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = "Parameter not supported";
}
public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = theUnsupportedReason;
}
public boolean supported() {
return mySupported;
}
public boolean matched() {
return myMatch;
}
public String getUnsupportedReason() {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason;
}
}

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SubscriptionMatcherCompositeInMemoryDatabase implements ISubscriptionMatcher {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherCompositeInMemoryDatabase.class);
@Autowired
SubscriptionMatcherDatabase mySubscriptionMatcherDatabase;
@Autowired
SubscriptionMatcherInMemory mySubscriptionMatcherInMemory;
@Autowired
DaoConfig myDaoConfig;
@Override
public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) {
SubscriptionMatchResult result;
if (myDaoConfig.isEnableInMemorySubscriptionMatching()) {
result = mySubscriptionMatcherInMemory.match(criteria, msg);
if (!result.supported()) {
ourLog.info("Criteria {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", criteria, result.getUnsupportedReason());
result = mySubscriptionMatcherDatabase.match(criteria, msg);
}
} else {
result = mySubscriptionMatcherDatabase.match(criteria, msg);
}
return result;
}
}

View File

@ -20,37 +20,39 @@ package ca.uhn.fhir.jpa.subscription.matcher;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.dao.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@Service
@Lazy
public class SubscriptionMatcherDatabase implements ISubscriptionMatcher {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherDatabase.class);
private final IFhirResourceDao<?> mySubscriptionDao;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
public SubscriptionMatcherDatabase(IFhirResourceDao<?> theSubscriptionDao, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
mySubscriptionDao = theSubscriptionDao;
mySubscriptionInterceptor = theSubscriptionInterceptor;
}
@Autowired
private FhirContext myCtx;
@Autowired
DaoRegistry myDaoRegistry;
@Autowired
MatchUrlService myMatchUrlService;
@Override
public boolean match(String criteria, ResourceModifiedMessage msg) {
IIdType id = msg.getId(getContext());
public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) {
IIdType id = msg.getId(myCtx);
String resourceType = id.getResourceType();
String resourceId = id.getIdPart();
@ -61,27 +63,23 @@ public class SubscriptionMatcherDatabase implements ISubscriptionMatcher {
ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria);
return results.size() > 0;
return new SubscriptionMatchResult(results.size() > 0);
}
/**
* Search based on a query criteria
*/
protected IBundleProvider performSearch(String theCriteria) {
RuntimeResourceDefinition responseResourceDef = mySubscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = BaseHapiFhirDao.translateMatchUrl(mySubscriptionDao, getContext(), theCriteria, responseResourceDef);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getResourceDao("Subscription");
RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef);
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
IFhirResourceDao<? extends IBaseResource> responseDao = mySubscriptionInterceptor.getDao(responseResourceDef.getImplementingClass());
IFhirResourceDao<? extends IBaseResource> responseDao = myDaoRegistry.getResourceDao(responseResourceDef.getImplementingClass());
responseCriteriaUrl.setLoadSynchronousUpTo(1);
IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req);
return responseResults;
}
public FhirContext getContext() {
return mySubscriptionDao.getContext();
return responseDao.search(responseCriteriaUrl, req);
}
}

View File

@ -20,13 +20,43 @@ package ca.uhn.fhir.jpa.subscription.matcher;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.index.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.dao.index.SearchParamExtractorService;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
@Lazy
public class SubscriptionMatcherInMemory implements ISubscriptionMatcher {
@Autowired
private FhirContext myContext;
@Autowired
private CriteriaResourceMatcher myCriteriaResourceMatcher;
@Autowired
private SearchParamExtractorService mySearchParamExtractorService;
@Override
public boolean match(String criteria, ResourceModifiedMessage msg) {
// FIXME KHS implement
return true;
public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) {
return match(criteria, msg.getNewPayload(myContext));
}
SubscriptionMatchResult match(String criteria, IBaseResource resource) {
ResourceTable entity = new ResourceTable();
String resourceType = myContext.getResourceDefinition(resource).getName();
entity.setResourceType(resourceType);
ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams();
mySearchParamExtractorService.extractFromResource(searchParams, entity, resource);
mySearchParamExtractorService.extractInlineReferences(resource);
mySearchParamExtractorService.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), false);
RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(resource);
return myCriteriaResourceMatcher.match(criteria, resourceDefinition, searchParams);
}
}

View File

@ -22,7 +22,10 @@ package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.subscription.*;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.client.api.*;
@ -35,7 +38,9 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
@ -45,11 +50,13 @@ import java.util.Map;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Component
@Scope("prototype")
public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDeliverySubscriber {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class);
public SubscriptionDeliveringRestHookSubscriber(IFhirResourceDao<?> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theSubscriptionDao, theChannelType, theSubscriptionInterceptor);
public SubscriptionDeliveringRestHookSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
super(theChannelType, theSubscriptionInterceptor);
}
protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) {

View File

@ -24,16 +24,25 @@ import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
@Lazy
public class SubscriptionRestHookInterceptor extends BaseSubscriptionInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRestHookInterceptor.class);
@Autowired
BeanFactory myBeanFactory;
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
SubscriptionDeliveringRestHookSubscriber value = new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getChannelType(), this);
SubscriptionDeliveringRestHookSubscriber value = myBeanFactory.getBean(SubscriptionDeliveringRestHookSubscriber.class, getChannelType(), this);
return Optional.of(value);
}

View File

@ -26,11 +26,15 @@ import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.Optional;
@Component
@Lazy
public class SubscriptionWebsocketInterceptor extends BaseSubscriptionInterceptor {
@Autowired

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.CoverageIgnore;
@ -21,9 +20,6 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -53,12 +49,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class HapiTerminologySvcDstu3 extends BaseHapiTerminologySvcImpl implements IValidationSupport, IHapiTerminologySvcDstu3 {
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
protected FhirContext myContext;
@Autowired
protected ITermCodeSystemDao myCodeSystemDao;
@Autowired
@Qualifier("myValueSetDaoDstu3")
private IFhirResourceDao<ValueSet> myValueSetResourceDao;

View File

@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.util.CoverageIgnore;
import ca.uhn.fhir.util.UrlUtil;
@ -20,9 +19,6 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -51,10 +47,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
public class HapiTerminologySvcR4 extends BaseHapiTerminologySvcImpl implements IHapiTerminologySvcR4 {
@Autowired
protected ITermCodeSystemDao myCodeSystemDao;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
@Autowired
@Qualifier("myConceptMapDaoR4")
private IFhirResourceDao<ConceptMap> myConceptMapResourceDao;
@ -68,8 +60,6 @@ public class HapiTerminologySvcR4 extends BaseHapiTerminologySvcImpl implements
private IValidationSupport myValidationSupport;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Autowired
private FhirContext myContext;
private void addAllChildren(String theSystemString, ConceptDefinitionComponent theCode, List<VersionIndependentConcept> theListToPopulate) {
if (isNotBlank(theCode.getCode())) {

View File

@ -188,7 +188,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc {
@Override
public UploadStatistics loadLoinc(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) {
LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles);
try (LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles)) {
List<String> mandatoryFilenameFragments = Arrays.asList(
LOINC_FILE,
LOINC_HIERARCHY_FILE,
@ -216,10 +216,11 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc {
return processLoincFiles(descriptors, theRequestDetails);
}
}
@Override
public UploadStatistics loadSnomedCt(List<FileDescriptor> theFiles, RequestDetails theRequestDetails) {
LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles);
try (LoadedFileDescriptors descriptors = new LoadedFileDescriptors(theFiles)) {
List<String> expectedFilenameFragments = Arrays.asList(
SCT_FILE_DESCRIPTION,
@ -231,6 +232,7 @@ public class TerminologyLoaderSvcImpl implements IHapiTerminologyLoaderSvc {
return processSnomedCtFiles(descriptors, theRequestDetails);
}
}
UploadStatistics processLoincFiles(LoadedFileDescriptors theDescriptors, RequestDetails theRequestDetails) {
final TermCodeSystemVersion codeSystemVersion = new TermCodeSystemVersion();

View File

@ -1,42 +1,48 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.model.dstu2.composite.PeriodDt;
import ca.uhn.fhir.model.dstu2.resource.Condition;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.util.TestUtil;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.dstu2.composite.PeriodDt;
import ca.uhn.fhir.model.dstu2.resource.Condition;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.util.TestUtil;
import org.springframework.transaction.PlatformTransactionManager;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class BaseHapiFhirDaoTest extends BaseJpaTest {
private static FhirContext ourCtx = FhirContext.forDstu2();
@Autowired
MatchUrlService myMatchUrlService;
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@Test
public void testTranslateMatchUrl() {
RuntimeResourceDefinition resourceDef = ourCtx.getResourceDefinition(Condition.class);
IDao dao = mock(IDao.class);
when(dao.getSearchParamByName(any(RuntimeResourceDefinition.class), eq("patient"))).thenReturn(resourceDef.getSearchParam("patient"));
SearchParameterMap match = BaseHapiFhirDao.translateMatchUrl(dao, ourCtx, "Condition?patient=304&_lastUpdated=>2011-01-01T11:12:21.0000Z", resourceDef);
ISearchParamRegistry searchParamRegistry = mock(ISearchParamRegistry.class);
when(searchParamRegistry.getSearchParamByName(any(RuntimeResourceDefinition.class), eq("patient"))).thenReturn(resourceDef.getSearchParam("patient"));
SearchParameterMap match = myMatchUrlService.translateMatchUrl("Condition?patient=304&_lastUpdated=>2011-01-01T11:12:21.0000Z", resourceDef);
assertEquals("2011-01-01T11:12:21.0000Z", match.getLastUpdated().getLowerBound().getValueAsString());
assertEquals(ReferenceParam.class, match.get("patient").get(0).get(0).getClass());
assertEquals("304", ((ReferenceParam)match.get("patient").get(0).get(0)).getIdPart());

View File

@ -252,6 +252,8 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao;
@Autowired
private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3;
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@After()
public void afterCleanupDao() {

View File

@ -2,10 +2,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
@ -80,6 +77,16 @@ public class SearchParamExtractorDstu3Test {
public void requestRefresh() {
// nothing
}
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
return null;
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return null;
}
};
SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new DaoConfig(), ourCtx, ourValidationSupport, searchParamRegistry);

View File

@ -22,10 +22,7 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
@ -85,6 +82,16 @@ public class SearchParamExtractorR4Test {
public void requestRefresh() {
// nothing
}
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
return null;
}
@Override
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return null;
}
};
}

View File

@ -0,0 +1,28 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.rest.param.HasParam;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class SearchParameterMapTest {
@Autowired
FhirContext myContext;
@Test
public void toNormalizedQueryStringTest() {
SearchParameterMap params = new SearchParameterMap();
params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO"));
String criteria = params.toNormalizedQueryString(myContext);
assertEquals(criteria, "?_has:Observation:identifier:urn:system|FOO=urn%3Asystem%7CFOO");
}
}

View File

@ -39,9 +39,10 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
private static final Logger ourLog = LoggerFactory.getLogger(EmailSubscriptionDstu2Test.class);
private static GreenMail ourTestSmtp;
private static int ourListenerPort;
private SubscriptionEmailInterceptor mySubscriber;
private List<IIdType> mySubscriptionIds = new ArrayList<>();
@Autowired
private SubscriptionEmailInterceptor mySubscriber;
@Autowired
private List<IFhirResourceDao<?>> myResourceDaos;
@Autowired
@ -70,7 +71,6 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
emailSender.setSmtpServerPort(ourListenerPort);
emailSender.start();
mySubscriber = new SubscriptionEmailInterceptor();
mySubscriber.setEmailSender(emailSender);
mySubscriber.setResourceDaos(myResourceDaos);
mySubscriber.setFhirContext(myFhirCtx);

View File

@ -0,0 +1,486 @@
package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.codesystems.MedicationRequestCategory;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SubscriptionMatcherInMemoryTestR3 extends BaseResourceProviderDstu3Test {
@Autowired
SubscriptionMatcherInMemory mySubscriptionMatcherInMemory;
private void assertUnsupported(IBaseResource resource, String criteria) {
assertFalse(mySubscriptionMatcherInMemory.match(criteria, resource).supported());
}
private void assertMatched(IBaseResource resource, String criteria) {
SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource);
;
assertTrue(result.supported());
assertTrue(result.matched());
}
private void assertNotMatched(IBaseResource resource, String criteria) {
SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource);
;
assertTrue(result.supported());
assertFalse(result.matched());
}
/*
The following tests are copied from an e-mail from a site using HAPI FHIR
*/
@Test
public void testQuestionnaireResponse() {
String criteria = "QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord";
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/HomeAbsenceHospitalizationRecord");
assertMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/Other");
assertNotMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setDisplay("Questionnaire/HomeAbsenceHospitalizationRecord");
assertNotMatched(qr, criteria);
}
}
@Test
public void testCommunicationRequest() {
String criteria = "CommunicationRequest?occurrence==2018-10-17";
{
CommunicationRequest cr = new CommunicationRequest();
cr.setOccurrence(new DateTimeType("2018-10-17"));
assertMatched(cr, criteria);
}
{
CommunicationRequest cr = new CommunicationRequest();
cr.setOccurrence(new DateTimeType("2018-10-16"));
assertNotMatched(cr, criteria);
}
{
CommunicationRequest cr = new CommunicationRequest();
cr.setOccurrence(new DateTimeType("2018-10-16"));
assertNotMatched(cr, criteria);
}
}
@Test
public void testProcedureRequest() {
String criteria = "ProcedureRequest?intent=original-order";
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER);
assertMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORDER);
assertNotMatched(pr, criteria);
}
}
@Test
public void testObservationContextTypeUnsupported() {
String criteria = "Observation?code=17861-6&context.type=IHD";
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
}
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("17861-6");
assertUnsupported(obs, criteria);
}
}
// Check that it still fails fast even if the chained parameter is first
@Test
public void testObservationContextTypeUnsupportedReverse() {
String criteria = "Observation?context.type=IHD&code=17861-6";
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
}
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("17861-6");
assertUnsupported(obs, criteria);
}
}
@Test
public void medicationRequestOutpatient() {
// Note the date== evaluates to date=eq which is a legacy format supported by hapi fhir
String criteria = "MedicationRequest?intent=instance-order&category=outpatient&date==2018-10-19";
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
Dosage dosage = new Dosage();
Timing timing = new Timing();
timing.getEvent().add(new DateTimeType("2018-10-19"));
dosage.setTiming(timing);
mr.getDosageInstruction().add(dosage);
assertMatched(mr, criteria);
}
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.INPATIENT.toCode());
Dosage dosage = new Dosage();
Timing timing = new Timing();
timing.getEvent().add(new DateTimeType("2018-10-19"));
dosage.setTiming(timing);
mr.getDosageInstruction().add(dosage);
assertNotMatched(mr, criteria);
}
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.INSTANCEORDER);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
Dosage dosage = new Dosage();
Timing timing = new Timing();
timing.getEvent().add(new DateTimeType("2018-10-20"));
dosage.setTiming(timing);
mr.getDosageInstruction().add(dosage);
assertNotMatched(mr, criteria);
}
}
@Test
public void testMedicationRequestStatuses() {
String criteria = "MedicationRequest?intent=plan&category=outpatient&status=suspended,entered-in-error,cancelled,stopped";
// Note suspended is an invalid status and will never match
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
mr.setStatus(MedicationRequest.MedicationRequestStatus.ENTEREDINERROR);
assertMatched(mr, criteria);
}
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
mr.setStatus(MedicationRequest.MedicationRequestStatus.CANCELLED);
assertMatched(mr, criteria);
}
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
mr.setStatus(MedicationRequest.MedicationRequestStatus.STOPPED);
assertMatched(mr, criteria);
}
{
MedicationRequest mr = new MedicationRequest();
mr.setIntent(MedicationRequest.MedicationRequestIntent.PLAN);
mr.getCategory().addCoding().setCode(MedicationRequestCategory.OUTPATIENT.toCode());
mr.setStatus(MedicationRequest.MedicationRequestStatus.ACTIVE);
assertNotMatched(mr, criteria);
}
}
@Test
public void testBloodTest() {
String criteria = "Observation?code=FR_Org1Blood2nd,FR_Org1Blood3rd,FR_Org%201BldCult,FR_Org2Blood2nd,FR_Org2Blood3rd,FR_Org%202BldCult,FR_Org3Blood2nd,FR_Org3Blood3rd,FR_Org3BldCult,FR_Org4Blood2nd,FR_Org4Blood3rd,FR_Org4BldCult,FR_Org5Blood2nd,FR_Org5Blood3rd,FR_Org%205BldCult,FR_Org6Blood2nd,FR_Org6Blood3rd,FR_Org6BldCult,FR_Org7Blood2nd,FR_Org7Blood3rd,FR_Org7BldCult,FR_Org8Blood2nd,FR_Org8Blood3rd,FR_Org8BldCult,FR_Org9Blood2nd,FR_Org9Blood3rd,FR_Org9BldCult,FR_Bld2ndCulture,FR_Bld3rdCulture,FR_Blood%20Culture,FR_Com1Bld3rd,FR_Com1BldCult,FR_Com2Bld2nd,FR_Com2Bld3rd,FR_Com2BldCult,FR_CultureBld2nd,FR_CultureBld3rd,FR_CultureBldCul,FR_GmStainBldCul,FR_GramStain2Bld,FR_GramStain3Bld,FR_GramStNegBac&context.type=IHD";
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("FR_Org1Blood2nd");
assertUnsupported(obs, criteria);
}
{
Observation obs = new Observation();
obs.getCode().addCoding().setCode("XXX");
assertNotMatched(obs, criteria);
}
}
@Test
public void testProcedureHemodialysis() {
String criteria = "Procedure?category=Hemodialysis";
{
Procedure proc = new Procedure();
proc.getCategory().addCoding().setCode("Hemodialysis");
assertMatched(proc, criteria);
}
{
Procedure proc = new Procedure();
proc.getCategory().addCoding().setCode("XXX");
assertNotMatched(proc, criteria);
}
}
@Test
public void testProcedureHDStandard() {
String criteria = "Procedure?code=HD_Standard&status=completed&location=Lab123";
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("HD_Standard");
proc.setStatus(Procedure.ProcedureStatus.COMPLETED);
IIdType locId = new IdType("Location", "Lab123");
proc.getLocation().setReference(locId.getValue());
assertMatched(proc, criteria);
}
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("HD_Standard");
proc.setStatus(Procedure.ProcedureStatus.COMPLETED);
IIdType locId = new IdType("Location", "XXX");
proc.getLocation().setReference(locId.getValue());
assertNotMatched(proc, criteria);
}
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("XXX");
proc.setStatus(Procedure.ProcedureStatus.COMPLETED);
IIdType locId = new IdType("Location", "Lab123");
proc.getLocation().setReference(locId.getValue());
assertNotMatched(proc, criteria);
}
}
@Test
public void testProvenance() {
String criteria = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
SearchParameter sp = new SearchParameter();
sp.addBase("Provenance");
sp.setCode("activity");
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("Provenance.activity");
sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(sp);
mySearchParamRegsitry.forceRefresh();
{
Provenance prov = new Provenance();
prov.setActivity(new Coding().setSystem("http://hl7.org/fhir/v3/DocumentCompletion").setCode("AU"));
assertMatched(prov, criteria);
}
{
Provenance prov = new Provenance();
assertNotMatched(prov, criteria);
}
{
Provenance prov = new Provenance();
prov.setActivity(new Coding().setCode("XXX"));
assertNotMatched(prov, criteria);
}
}
@Test
public void testBodySite() {
String criteria = "BodySite?accessType=Catheter,PD%20Catheter";
SearchParameter sp = new SearchParameter();
sp.addBase("BodySite");
sp.setCode("accessType");
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("BodySite.extension('BodySite#accessType')");
sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(sp);
mySearchParamRegsitry.forceRefresh();
{
BodySite bodySite = new BodySite();
bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("Catheter"));
assertMatched(bodySite, criteria);
}
{
BodySite bodySite = new BodySite();
bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("PD Catheter"));
assertMatched(bodySite, criteria);
}
{
BodySite bodySite = new BodySite();
assertNotMatched(bodySite, criteria);
}
{
BodySite bodySite = new BodySite();
bodySite.addExtension().setUrl("BodySite#accessType").setValue(new Coding().setCode("XXX"));
assertNotMatched(bodySite, criteria);
}
}
@Test
public void testProcedureAnyLocation() {
String criteria = "Procedure?code=HD_Standard&status=completed";
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("HD_Standard");
proc.setStatus(Procedure.ProcedureStatus.COMPLETED);
IIdType locId = new IdType("Location", "Lab456");
proc.getLocation().setReference(locId.getValue());
assertMatched(proc, criteria);
}
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("HD_Standard");
proc.setStatus(Procedure.ProcedureStatus.ABORTED);
assertNotMatched(proc, criteria);
}
{
Procedure proc = new Procedure();
proc.getCode().addCoding().setCode("XXX");
proc.setStatus(Procedure.ProcedureStatus.COMPLETED);
assertNotMatched(proc, criteria);
}
}
@Test
public void testQuestionnaireResponseLong() {
String criteria = "QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord,FMCSWDepressionSymptomsScreener,FMCAKIComprehensiveSW,FMCSWIntensiveScreener,FMCESRDComprehensiveSW,FMCNutritionProgressNote,FMCAKIComprehensiveRN";
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/HomeAbsenceHospitalizationRecord");
assertMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/FMCSWIntensiveScreener");
assertMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/FMCAKIComprehensiveRN");
assertMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
assertNotMatched(qr, criteria);
}
{
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.getQuestionnaire().setReference("Questionnaire/FMCAKIComprehensiveRM");
assertNotMatched(qr, criteria);
}
}
@Test
public void testProcedureRequestCategory() {
String criteria = "ProcedureRequest?intent=instance-order&category=Laboratory,Ancillary%20Orders,Hemodialysis&occurrence==2018-10-19";
SearchParameter sp = new SearchParameter();
sp.addBase("ProcedureRequest");
sp.setCode("category");
sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("ProcedureRequest.category");
sp.setXpathUsage(org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(sp);
mySearchParamRegsitry.forceRefresh();
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("Laboratory");
pr.getCategory().add(code);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("Ancillary Orders");
pr.getCategory().add(code);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("Hemodialysis");
pr.getCategory().add(code);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertNotMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("Hemodialysis");
pr.getCategory().add(code);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertNotMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("Hemodialysis");
pr.getCategory().add(code);
assertNotMatched(pr, criteria);
}
{
ProcedureRequest pr = new ProcedureRequest();
pr.setIntent(ProcedureRequest.ProcedureRequestIntent.INSTANCEORDER);
CodeableConcept code = new CodeableConcept();
code.addCoding().setCode("XXX");
pr.getCategory().add(code);
pr.setOccurrence(new DateTimeType("2018-10-19"));
assertNotMatched(pr, criteria);
}
}
@Test
public void testEposideOfCare() {
String criteria = "EpisodeOfCare?status=active";
{
EpisodeOfCare eoc = new EpisodeOfCare();
eoc.setStatus(EpisodeOfCare.EpisodeOfCareStatus.ACTIVE);
assertMatched(eoc, criteria);
}
{
EpisodeOfCare eoc = new EpisodeOfCare();
assertNotMatched(eoc, criteria);
}
{
EpisodeOfCare eoc = new EpisodeOfCare();
eoc.setStatus(EpisodeOfCare.EpisodeOfCareStatus.CANCELLED);
assertNotMatched(eoc, criteria);
}
}
// These last two are covered by other tests above
// String criteria = "ProcedureRequest?intent=original-order&category=Laboratory,Ancillary%20Orders,Hemodialysis&status=suspended,entered-in-error,cancelled";
// String criteria = "Observation?code=70965-9&context.type=IHD";
}

View File

@ -0,0 +1,889 @@
package ca.uhn.fhir.jpa.subscription.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class SubscriptionMatcherInMemoryTestR4 {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionMatcherInMemoryTestR4.class);
@Autowired
SubscriptionMatcherInMemory mySubscriptionMatcherInMemory;
@Autowired
FhirContext myContext;
private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) {
String criteria = params.toNormalizedQueryString(myContext);
ourLog.info("Criteria: <{}>", criteria);
return mySubscriptionMatcherInMemory.match(criteria, resource);
}
private void assertUnsupported(IBaseResource resource, SearchParameterMap params) {
assertFalse(match(resource, params).supported());
}
private void assertMatched(IBaseResource resource, SearchParameterMap params) {
SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported());
assertTrue(result.matched());
}
private void assertNotMatched(IBaseResource resource, SearchParameterMap params) {
SubscriptionMatchResult result = match(resource, params);
assertTrue(result.getUnsupportedReason(), result.supported());
assertFalse(result.matched());
}
/*
The following tests are copied from FhirResourceDaoR4SearchNoFtTest
*/
@Test
public void testChainReferenceUnsupported() {
Encounter enc1 = new Encounter();
IIdType pid1 = new IdType("Patient", 1L);
enc1.getSubject().setReference(pid1.getValue());
SearchParameterMap map;
map = new SearchParameterMap();
map.add(Encounter.SP_SUBJECT, new ReferenceParam("subject", "foo|bar").setChain("identifier"));
assertUnsupported(enc1, map);
MedicationAdministration ma = new MedicationAdministration();
IIdType mid1 = new IdType("Medication", 1L);
ma.setMedication(new Reference(mid1));
map = new SearchParameterMap();
map.add(MedicationAdministration.SP_MEDICATION, new ReferenceAndListParam().addAnd(new ReferenceOrListParam().add(new ReferenceParam("code", "04823543"))));
assertUnsupported(ma, map);
}
@Test
public void testHasParameterUnsupported() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe");
SearchParameterMap params = new SearchParameterMap();
params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO"));
String criteria = params.toNormalizedQueryString(myContext);
assertUnsupported(patient, params);
}
@Test
public void testSearchCode() {
Subscription subs = new Subscription();
subs.setStatus(Subscription.SubscriptionStatus.ACTIVE);
subs.getChannel().setType(Subscription.SubscriptionChannelType.WEBSOCKET);
subs.setCriteria("Observation?");
SearchParameterMap params = new SearchParameterMap();
assertMatched(subs, params);
params = new SearchParameterMap();
params.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode()));
params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode()));
assertMatched(subs, params);
params = new SearchParameterMap();
params.add(Subscription.SP_TYPE, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode()));
params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode() + "2"));
assertNotMatched(subs, params);
// // Wrong param
params = new SearchParameterMap();
params.add(Subscription.SP_STATUS, new TokenParam(null, Subscription.SubscriptionChannelType.WEBSOCKET.toCode()));
assertNotMatched(subs, params);
}
@Test
public void testSearchCompositeUnsupported() {
Observation o1 = new Observation();
o1.getCode().addCoding().setSystem("foo").setCode("testSearchCompositeParamN01");
o1.setValue(new StringType("testSearchCompositeParamS01"));
TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01");
StringParam v1 = new StringParam("testSearchCompositeParamS01");
CompositeParam<TokenParam, StringParam> val = new CompositeParam<TokenParam, StringParam>(v0, v1);
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val);
assertUnsupported(o1, params);
}
@Test
public void testComponentQuantityWithPrefixUnsupported() {
Observation o1 = new Observation();
o1.addComponent()
.setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1")))
.setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(200));
o1.addComponent()
.setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2")))
.setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(200));
String param = Observation.SP_COMPONENT_VALUE_QUANTITY;
QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1");
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
assertUnsupported(o1, params);
}
@Test
public void testComponentQuantityEquals() {
Observation o1 = new Observation();
o1.addComponent()
.setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code1")))
.setValue(new Quantity().setSystem("http://bar").setCode("code1").setValue(150));
o1.addComponent()
.setCode(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("code2")))
.setValue(new Quantity().setSystem("http://bar").setCode("code2").setValue(150));
String param = Observation.SP_COMPONENT_VALUE_QUANTITY;
QuantityParam v1 = new QuantityParam(null, 150, "http://bar", "code1");
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(param, v1);
assertMatched(o1, params);
}
@Test
public void testIdNotSupported() {
Observation o1 = new Observation();
SearchParameterMap params = new SearchParameterMap();
params.add("_id", new StringParam("testSearchForUnknownAlphanumericId"));
assertUnsupported(o1, params);
}
@Test
public void testLanguageNotSupported() {
Patient patient = new Patient();
patient.getLanguageElement().setValue("en_CA");
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("testSearchLanguageParam").addGiven("Joe");
SearchParameterMap params;
params = new SearchParameterMap();
params.add(IAnyResource.SP_RES_LANGUAGE, new StringParam("en_CA"));
assertUnsupported(patient, params);
}
@Test
public void testSearchLastUpdatedParamUnsupported() throws InterruptedException {
String methodName = "testSearchLastUpdatedParam";
DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY);
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily(methodName).addGiven("Joe");
SearchParameterMap params = new SearchParameterMap();
params.setLastUpdated(new DateRangeParam(today, null));
assertUnsupported(patient, params);
}
@Test
public void testSearchNameParam() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("testSearchNameParam01Fam").addGiven("testSearchNameParam01Giv");
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam"));
assertMatched(patient, params);
// Given name shouldn't return for family param
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Giv"));
assertNotMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Fam"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_NAME, new StringParam("testSearchNameParam01Giv"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Foo"));
assertNotMatched(patient, params);
}
@Test
public void testSearchNumberParam() {
RiskAssessment risk = new RiskAssessment();
risk.addIdentifier().setSystem("foo").setValue("testSearchNumberParam01");
risk.addPrediction().setProbability(new DecimalType(2));
SearchParameterMap params;
params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam(">1"));
assertUnsupported(risk, params);
params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("<1"));
assertUnsupported(risk, params);
params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("2"));
assertMatched(risk, params);
params = new SearchParameterMap().add(RiskAssessment.SP_PROBABILITY, new NumberParam("3"));
assertNotMatched(risk, params);
}
@Test
public void testSearchNumberWrongParam() {
ImmunizationRecommendation ir1 = new ImmunizationRecommendation();
ir1.addRecommendation().setDoseNumber(new PositiveIntType(1));
SearchParameterMap params = new SearchParameterMap().add(ImmunizationRecommendation.SP_DOSE_NUMBER, new NumberParam("1"));
assertMatched(ir1, params);
params = new SearchParameterMap().add(ImmunizationRecommendation.SP_DOSE_SEQUENCE, new NumberParam("1"));
assertNotMatched(ir1, params);
}
@Test
public void testSearchPractitionerPhoneAndEmailParam() {
String methodName = "testSearchPractitionerPhoneAndEmailParam";
Practitioner patient = new Practitioner();
patient.addName().setFamily(methodName);
patient.addTelecom().setSystem(ContactPoint.ContactPointSystem.PHONE).setValue("123");
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Practitioner.SP_FAMILY, new StringParam(methodName));
params.add(Practitioner.SP_EMAIL, new TokenParam(null, "123"));
assertNotMatched(patient, params);
params = new SearchParameterMap();
params.add(Practitioner.SP_FAMILY, new StringParam(methodName));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Practitioner.SP_FAMILY, new StringParam(methodName));
params.add(Practitioner.SP_EMAIL, new TokenParam(null, "abc"));
assertNotMatched(patient, params);
params = new SearchParameterMap();
params.add(Practitioner.SP_FAMILY, new StringParam(methodName));
params.add(Practitioner.SP_PHONE, new TokenParam(null, "123"));
assertMatched(patient, params);
}
@Test
public void testSearchQuantityWrongParam() {
Condition c1 = new Condition();
c1.setAbatement(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L)));
SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1"));
assertMatched(c1, params);
Condition c2 = new Condition();
c2.setOnset(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L)));
params = new SearchParameterMap().add(Condition.SP_ONSET_AGE, new QuantityParam("1"));
assertMatched(c2, params);
}
@Test
public void testSearchResourceLinkWithChainUnsupported() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX");
patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain01");
IIdType patientId01 = new IdType("Patient", 1L);
patient.setId(patientId01);
Patient patient02 = new Patient();
patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChainXX");
patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithChain02");
IIdType patientId02 = new IdType("Patient", 2L);
patient02.setId(patientId02);
Observation obs01 = new Observation();
obs01.setEffective(new DateTimeType(new Date()));
obs01.setSubject(new Reference(patientId01));
Observation obs02 = new Observation();
obs02.setEffective(new DateTimeType(new Date()));
obs02.setSubject(new Reference(patientId02));
SearchParameterMap params = new SearchParameterMap().add(Observation.SP_SUBJECT, new ReferenceParam(Patient.SP_IDENTIFIER, "urn:system|testSearchResourceLinkWithChain01"));
assertUnsupported(obs01, params);
}
@Test
public void testSearchResourceLinkWithTextLogicalId() {
Patient patient = new Patient();
String patientName01 = "testSearchResourceLinkWithTextLogicalId01";
patient.setId(patientName01);
patient.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX");
patient.addIdentifier().setSystem("urn:system").setValue(patientName01);
IIdType patientId01 = new IdType("Patient", patientName01);
Patient patient02 = new Patient();
String patientName02 = "testSearchResourceLinkWithTextLogicalId02";
patient02.setId(patientName02);
patient02.addIdentifier().setSystem("urn:system").setValue("testSearchResourceLinkWithTextLogicalIdXX");
patient02.addIdentifier().setSystem("urn:system").setValue(patientName02);
IIdType patientId02 = new IdType("Patient", patientName02);
Observation obs01 = new Observation();
obs01.setEffective(new DateTimeType(new Date()));
obs01.setSubject(new Reference(patientId01));
Observation obs02 = new Observation();
obs02.setEffective(new DateTimeType(new Date()));
obs02.setSubject(new Reference(patientId02));
SearchParameterMap params = new SearchParameterMap().add(Observation.SP_SUBJECT, new ReferenceParam(patientName01));
assertMatched(obs01, params);
assertNotMatched(obs02, params);
params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId99"));
assertNotMatched(obs01, params);
assertNotMatched(obs02, params);
params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("999999999999999"));
assertNotMatched(obs01, params);
assertNotMatched(obs02, params);
}
@Test
public void testSearchResourceReferenceOnlyCorrectPath() {
Organization org = new Organization();
org.setActive(true);
IIdType oid1 = new IdType("Organization", 1L);
Task task = new Task();
task.setRequester(new Reference(oid1));
Task task2 = new Task();
task2.setOwner(new Reference(oid1));
SearchParameterMap map;
map = new SearchParameterMap();
map.add(Task.SP_REQUESTER, new ReferenceParam(oid1.getValue()));
assertMatched(task, map);
assertNotMatched(task2, map);
}
@Test
public void testSearchStringParam() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe");
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_testSearchStringParam"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("FOO_testSearchStringParam"));
assertNotMatched(patient, params);
// Try with different casing
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("tester_testsearchstringparam"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("TESTER_TESTSEARCHSTRINGPARAM"));
assertMatched(patient, params);
}
@Test
public void testSearchStringParamReallyLong() {
String methodName = "testSearchStringParamReallyLong";
String value = StringUtils.rightPad(methodName, 200, 'a');
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily(value);
SearchParameterMap params;
params = new SearchParameterMap();
String substring = value.substring(0, ResourceIndexedSearchParamString.MAX_LENGTH);
params.add(Patient.SP_FAMILY, new StringParam(substring));
assertMatched(patient, params);
}
@Test
public void testSearchStringParamWithNonNormalized() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().addGiven("testSearchStringParamWithNonNormalized_h\u00F6ra");
Patient patient2 = new Patient();
patient2.addIdentifier().setSystem("urn:system").setValue("002");
patient2.addName().addGiven("testSearchStringParamWithNonNormalized_HORA");
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_GIVEN, new StringParam("testSearchStringParamWithNonNormalized_hora"));
assertMatched(patient, params);
assertMatched(patient2, params);
}
@Test
public void testSearchTokenParam() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1");
patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem")
.setDisplay("testSearchTokenParamDisplay");
Patient patient2 = new Patient();
patient2.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002");
patient2.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", "testSearchTokenParam001"));
assertMatched(patient, map);
assertNotMatched(patient2, map);
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam(null, "testSearchTokenParam001"));
assertMatched(patient, map);
assertNotMatched(patient2, map);
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_LANGUAGE, new TokenParam("testSearchTokenParamSystem", "testSearchTokenParamCode"));
assertMatched(patient, map);
assertNotMatched(patient2, map);
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_LANGUAGE, new TokenParam(null, "testSearchTokenParamCode", true));
assertUnsupported(patient, map);
}
{
SearchParameterMap map = new SearchParameterMap();
TokenOrListParam listParam = new TokenOrListParam();
listParam.add("urn:system", "testSearchTokenParam001");
listParam.add("urn:system", "testSearchTokenParam002");
map.add(Patient.SP_IDENTIFIER, listParam);
assertMatched(patient, map);
assertMatched(patient2, map);
}
{
SearchParameterMap map = new SearchParameterMap();
TokenOrListParam listParam = new TokenOrListParam();
listParam.add(null, "testSearchTokenParam001");
listParam.add("urn:system", "testSearchTokenParam002");
map.add(Patient.SP_IDENTIFIER, listParam);
assertMatched(patient, map);
assertMatched(patient2, map);
}
}
@Test
public void testSearchTokenParamNoValue() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam001");
patient.addName().setFamily("Tester").addGiven("testSearchTokenParam1");
patient.addCommunication().getLanguage().setText("testSearchTokenParamComText").addCoding().setCode("testSearchTokenParamCode").setSystem("testSearchTokenParamSystem")
.setDisplay("testSearchTokenParamDisplay");
Patient patient2 = new Patient();
patient2.addIdentifier().setSystem("urn:system").setValue("testSearchTokenParam002");
patient2.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
Patient patient3 = new Patient();
patient3.addIdentifier().setSystem("urn:system2").setValue("testSearchTokenParam002");
patient3.addName().setFamily("Tester").addGiven("testSearchTokenParam2");
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", null));
// Match 2
assertMatched(patient, map);
assertMatched(patient2, map);
assertNotMatched(patient3, map);
}
{
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_IDENTIFIER, new TokenParam("urn:system", ""));
// Match 2
assertMatched(patient, map);
assertMatched(patient2, map);
assertNotMatched(patient3, map);
}
}
@Test
public void testSearchTokenWithNotModifierUnsupported() {
String male, female;
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester").addGiven("Joe");
patient.setGender(Enumerations.AdministrativeGender.MALE);
List<String> patients;
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_GENDER, new TokenParam(null, "male"));
assertMatched(patient, params);
params = new SearchParameterMap();
params.add(Patient.SP_GENDER, new TokenParam(null, "male").setModifier(TokenParamModifier.NOT));
assertUnsupported(patient, params);
}
@Test
public void testSearchTokenWrongParam() {
Patient p1 = new Patient();
p1.setGender(Enumerations.AdministrativeGender.MALE);
Patient p2 = new Patient();
p2.addIdentifier().setValue(Enumerations.AdministrativeGender.MALE.toCode());
{
SearchParameterMap map = new SearchParameterMap().add(Patient.SP_GENDER, new TokenParam(null, "male"));
assertMatched(p1, map);
assertNotMatched(p2, map);
}
{
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(Patient.SP_IDENTIFIER, new TokenParam(null, "male"));
assertNotMatched(p1, map);
}
}
@Test
public void testSearchUriWrongParam() {
ValueSet v1 = new ValueSet();
v1.getUrlElement().setValue("http://foo");
ValueSet v2 = new ValueSet();
v2.getExpansion().getIdentifierElement().setValue("http://foo");
{
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_URL, new UriParam("http://foo"));
assertMatched(v1, map);
assertNotMatched(v2, map);
}
{
SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(ValueSet.SP_EXPANSION, new UriParam("http://foo"));
assertNotMatched(v1, map);
assertMatched(v2, map);
}
}
@Test
public void testSearchValueQuantity() {
String methodName = "testSearchValueQuantity";
Observation o1 = new Observation();
o1.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code");
Quantity q1 = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(10);
o1.setValue(q1);
Observation o2 = new Observation();
o2.getCode().addCoding().setSystem("urn:foo").setCode(methodName + "code");
Quantity q2 = new Quantity().setSystem("urn:bar:" + methodName).setCode(methodName + "units").setValue(5);
o2.setValue(q2);
SearchParameterMap map;
IBundleProvider found;
QuantityParam param;
map = new SearchParameterMap();
param = new QuantityParam(null, new BigDecimal("10"), null, null);
map.add(Observation.SP_VALUE_QUANTITY, param);
assertMatched(o1, map);
assertNotMatched(o2, map);
map = new SearchParameterMap();
param = new QuantityParam(null, new BigDecimal("10"), null, methodName + "units");
map.add(Observation.SP_VALUE_QUANTITY, param);
assertMatched(o1, map);
assertNotMatched(o2, map);
map = new SearchParameterMap();
map.setLoadSynchronous(true);
param = new QuantityParam(null, new BigDecimal("10"), "urn:bar:" + methodName, null);
map.add(Observation.SP_VALUE_QUANTITY, param);
assertMatched(o1, map);
assertNotMatched(o2, map);
map = new SearchParameterMap();
map.setLoadSynchronous(true);
param = new QuantityParam(null, new BigDecimal("10"), "urn:bar:" + methodName, methodName + "units");
map.add(Observation.SP_VALUE_QUANTITY, param);
assertMatched(o1, map);
assertNotMatched(o2, map);
map = new SearchParameterMap();
map.setLoadSynchronous(true);
param = new QuantityParam(null, new BigDecimal("1000"), "urn:bar:" + methodName, methodName + "units");
map.add(Observation.SP_VALUE_QUANTITY, param);
assertNotMatched(o1, map);
assertNotMatched(o2, map);
}
@Test
public void testSearchWithContainsUnsupported() {
Patient pt1 = new Patient();
pt1.addName().setFamily("ABCDEFGHIJK");
List<String> ids;
SearchParameterMap map;
IBundleProvider results;
// Contains = true
map = new SearchParameterMap();
map.add(Patient.SP_NAME, new StringParam("FGHIJK").setContains(true));
assertUnsupported(pt1, map);
}
@Test
public void testSearchWithDate() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
Patient patient2 = new Patient();
patient2.addIdentifier().setSystem("urn:system").setValue("002");
patient2.addName().setFamily("Tester_testSearchStringParam").addGiven("John");
patient2.setBirthDateElement(new DateType("2011-01-01"));
{
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-01"));
assertNotMatched(patient, params);
assertMatched(patient2, params);
}
{
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronous(true);
params.add(Patient.SP_BIRTHDATE, new DateParam("2011-01-03"));
assertNotMatched(patient, params);
assertNotMatched(patient2, params);
}
}
@Test
public void testSearchWithIncludesIgnored() {
String methodName = "testSearchWithIncludes";
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:system").setValue("001");
patient.addName().setFamily("Tester_" + methodName + "_P1").addGiven("Joe");
{
// No includes
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
assertMatched(patient, params);
}
{
// Named include
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(Patient.INCLUDE_ORGANIZATION.asNonRecursive());
assertMatched(patient, params);
}
{
// Named include with parent non-recursive
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(Patient.INCLUDE_ORGANIZATION);
params.addInclude(Organization.INCLUDE_PARTOF.asNonRecursive());
assertMatched(patient, params);
}
{
// Named include with parent recursive
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(Patient.INCLUDE_ORGANIZATION);
params.addInclude(Organization.INCLUDE_PARTOF.asRecursive());
assertMatched(patient, params);
}
{
// * include non recursive
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(IBaseResource.INCLUDE_ALL.asNonRecursive());
assertMatched(patient, params);
}
{
// * include recursive
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(IBaseResource.INCLUDE_ALL.asRecursive());
assertMatched(patient, params);
}
{
// Irrelevant include
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("Tester_" + methodName + "_P1"));
params.addInclude(Encounter.INCLUDE_EPISODE_OF_CARE);
assertMatched(patient, params);
}
}
@Test
public void testSearchWithSecurityAndProfileParamsUnsupported() {
String methodName = "testSearchWithSecurityAndProfileParams";
Organization org = new Organization();
org.getNameElement().setValue("FOO");
org.getMeta().addSecurity("urn:taglist", methodName + "1a", null);
{
SearchParameterMap params = new SearchParameterMap();
params.add("_security", new TokenParam("urn:taglist", methodName + "1a"));
assertUnsupported(org, params);
}
{
SearchParameterMap params = new SearchParameterMap();
params.add("_profile", new UriParam("http://" + methodName));
assertUnsupported(org, params);
}
}
@Test
public void testSearchWithTagParameterUnsupported() {
String methodName = "testSearchWithTagParameter";
Organization org = new Organization();
org.getNameElement().setValue("FOO");
org.getMeta().addTag("urn:taglist", methodName + "1a", null);
org.getMeta().addTag("urn:taglist", methodName + "1b", null);
{
// One tag
SearchParameterMap params = new SearchParameterMap();
params.add("_tag", new TokenParam("urn:taglist", methodName + "1a"));
assertUnsupported(org, params);
}
}
@Test
public void testSearchWithVeryLongUrlLonger() {
Patient p = new Patient();
p.addName().setFamily("A1");
SearchParameterMap map = new SearchParameterMap();
StringOrListParam or = new StringOrListParam();
or.addOr(new StringParam("A1"));
for (int i = 0; i < 50; i++) {
or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i))));
}
map.add(Patient.SP_NAME, or);
assertMatched(p, map);
map = new SearchParameterMap();
or = new StringOrListParam();
or.addOr(new StringParam("A1"));
or.addOr(new StringParam("A1"));
for (int i = 0; i < 50; i++) {
or.addOr(new StringParam(StringUtils.leftPad("", 200, (char) ('A' + i))));
}
map.add(Patient.SP_NAME, or);
assertMatched(p, map);
}
@Test
public void testDateSearchParametersShouldBeTimezoneIndependent() {
List<Observation> nlist = new ArrayList<>();
nlist.add(createObservationWithEffective("NO1", "2011-01-02T23:00:00-11:30"));
nlist.add(createObservationWithEffective("NO2", "2011-01-03T00:00:00+01:00"));
List<Observation> ylist = new ArrayList<>();
ylist.add(createObservationWithEffective("YES01", "2011-01-02T00:00:00-11:30"));
ylist.add(createObservationWithEffective("YES02", "2011-01-02T00:00:00-10:00"));
ylist.add(createObservationWithEffective("YES03", "2011-01-02T00:00:00-09:00"));
ylist.add(createObservationWithEffective("YES04", "2011-01-02T00:00:00-08:00"));
ylist.add(createObservationWithEffective("YES05", "2011-01-02T00:00:00-07:00"));
ylist.add(createObservationWithEffective("YES06", "2011-01-02T00:00:00-06:00"));
ylist.add(createObservationWithEffective("YES07", "2011-01-02T00:00:00-05:00"));
ylist.add(createObservationWithEffective("YES08", "2011-01-02T00:00:00-04:00"));
ylist.add(createObservationWithEffective("YES09", "2011-01-02T00:00:00-03:00"));
ylist.add(createObservationWithEffective("YES10", "2011-01-02T00:00:00-02:00"));
ylist.add(createObservationWithEffective("YES11", "2011-01-02T00:00:00-01:00"));
ylist.add(createObservationWithEffective("YES12", "2011-01-02T00:00:00Z"));
ylist.add(createObservationWithEffective("YES13", "2011-01-02T00:00:00+01:00"));
ylist.add(createObservationWithEffective("YES14", "2011-01-02T00:00:00+02:00"));
ylist.add(createObservationWithEffective("YES15", "2011-01-02T00:00:00+03:00"));
ylist.add(createObservationWithEffective("YES16", "2011-01-02T00:00:00+04:00"));
ylist.add(createObservationWithEffective("YES17", "2011-01-02T00:00:00+05:00"));
ylist.add(createObservationWithEffective("YES18", "2011-01-02T00:00:00+06:00"));
ylist.add(createObservationWithEffective("YES19", "2011-01-02T00:00:00+07:00"));
ylist.add(createObservationWithEffective("YES20", "2011-01-02T00:00:00+08:00"));
ylist.add(createObservationWithEffective("YES21", "2011-01-02T00:00:00+09:00"));
ylist.add(createObservationWithEffective("YES22", "2011-01-02T00:00:00+10:00"));
ylist.add(createObservationWithEffective("YES23", "2011-01-02T00:00:00+11:00"));
SearchParameterMap map = new SearchParameterMap();
map.add(Observation.SP_DATE, new DateParam("2011-01-02"));
for (Observation obs : nlist) {
// assertNotMatched(obs, map);
}
for (Observation obs : ylist) {
ourLog.info("Obs {} has time {}", obs.getId(), obs.getEffectiveDateTimeType().getValue().toString());
assertMatched(obs, map);
}
}
private Observation createObservationWithEffective(String theId, String theEffective) {
Observation obs = new Observation();
obs.setId(theId);
obs.setEffective(new DateTimeType(theEffective));
return obs;
}
@Test
public void testSearchWithVeryLongUrlShorter() {
Patient p = new Patient();
p.addName().setFamily("A1");
SearchParameterMap map = new SearchParameterMap();
StringOrListParam or = new StringOrListParam();
or.addOr(new StringParam("A1"));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A')));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B')));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C')));
map.add(Patient.SP_NAME, or);
assertMatched(p, map);
map = new SearchParameterMap();
or = new StringOrListParam();
or.addOr(new StringParam("A1"));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'A')));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'B')));
or.addOr(new StringParam(StringUtils.leftPad("", 200, 'C')));
map.add(Patient.SP_NAME, or);
assertMatched(p, map);
}
}

View File

@ -13,9 +13,12 @@ import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.PortUtil;
import com.google.common.collect.Lists;
import net.ttddyy.dsproxy.QueryCount;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
@ -23,8 +26,10 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
@ -33,8 +38,7 @@ import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
/**
* Test the rest-hook subscriptions
@ -50,9 +54,35 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
private static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
private static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
private static List<String> ourHeaders = Collections.synchronizedList(new ArrayList<>());
private static SingleQueryCountHolder ourCountHolder;
private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
@Autowired
private SingleQueryCountHolder myCountHolder;
@Autowired
private DaoConfig myDaoConfig;
private CountingInterceptor myCountingInterceptor;
@PostConstruct
public void initializeOurCountHolder() {
ourCountHolder = myCountHolder;
}
@Before
public void enableInMemory() {
myDaoConfig.setEnableInMemorySubscriptionMatching(true);
}
@AfterClass
public static void reportTotalSelects() {
ourLog.info("Total database select queries: {}", getQueryCount().getSelect());
}
private static QueryCount getQueryCount() {
return ourCountHolder.getQueryCountMap().get("");
}
@After
public void afterUnregisterRestHookListener() {
for (IIdType next : mySubscriptionIds) {
@ -98,6 +128,16 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
}
private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException {
Subscription subscription = newSubscription(theCriteria, thePayload, theEndpoint);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId());
return subscription;
}
private Subscription newSubscription(String theCriteria, String thePayload, String theEndpoint) {
Subscription subscription = new Subscription();
subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)");
subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED);
@ -107,11 +147,6 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
channel.setPayload(thePayload);
channel.setEndpoint(theEndpoint);
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId());
return subscription;
}
@ -308,9 +343,9 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations);
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());
assertFalse(subscription1.getId().equals(subscription2.getId()));
assertFalse(observation1.getId().isEmpty());
assertFalse(observation2.getId().isEmpty());
}
@Test
@ -382,9 +417,9 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations);
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());
assertFalse(subscription1.getId().equals(subscription2.getId()));
assertFalse(observation1.getId().isEmpty());
assertFalse(observation2.getId().isEmpty());
}
@Test
@ -533,6 +568,30 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
RestHookTestDstu2Test.waitForQueueToDrain(getRestHookSubscriptionInterceptor());
}
@Test(expected= UnprocessableEntityException.class)
public void testInvalidProvenanceParam() {
String payload = "application/fhir+json";
String criteriabad = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU";
Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase);
ourClient.create().resource(subscription).execute();
}
@Test(expected= UnprocessableEntityException.class)
public void testInvalidProcedureRequestParam() {
String payload = "application/fhir+json";
String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory";
Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase);
ourClient.create().resource(subscription).execute();
}
@Test(expected= UnprocessableEntityException.class)
public void testInvalidBodySiteParam() {
String payload = "application/fhir+json";
String criteriabad = "BodySite?accessType=Catheter";
Subscription subscription = newSubscription(criteriabad, payload, ourListenerServerBase);
ourClient.create().resource(subscription).execute();
}
public static class ObservationListener implements IResourceProvider {
@Create

View File

@ -159,12 +159,6 @@ public class JpaServerDemo extends RestfulServer {
if (fhirVersion == FhirVersionEnum.DSTU3) {
registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class));
}
// Enable various subscription types
registerInterceptor(myAppCtx.getBean(SubscriptionWebsocketInterceptor.class));
registerInterceptor(myAppCtx.getBean(SubscriptionRestHookInterceptor.class));
registerInterceptor(myAppCtx.getBean(SubscriptionEmailInterceptor.class));
}
}

View File

@ -14,6 +14,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.spring.boot.autoconfigure.FhirAutoConfiguration.FhirJpaServerConfiguration.Dstu3;
import org.assertj.core.util.Arrays;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@ -55,10 +56,19 @@ public class FhirAutoConfigurationTest {
@Test
public void withFhirVersion() throws Exception {
load("hapi.fhir.version:DSTU3");
load(Arrays.array(EmbeddedDataSourceConfiguration.class,
HibernateJpaAutoConfiguration.class,
FhirAutoConfiguration.class),
"hapi.fhir.version:DSTU3", "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles",
"spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory");
assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.DSTU3.getVersionImplementation());
load("hapi.fhir.version:R4");
load(Arrays.array(EmbeddedDataSourceConfiguration.class,
HibernateJpaAutoConfiguration.class,
FhirAutoConfiguration.class),
"hapi.fhir.version:R4",
"spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles",
"spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory");
assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.R4.getVersionImplementation());
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@ -16,6 +16,20 @@
</ul>
]]>
</action>
<action type="add">
Changed subscription processing, if the subscription criteria are straightforward (i.e. no
chained references, qualifiers or prefixes) then attempt to match the incoming resource against
the criteria in-memory. If the subscription criteria can't be matched in-memory, then the
server falls back to the original subscription matching process of querying the database. The
in-memory matcher can be disabled by setting isEnableInMemorySubscriptionMatching to "false" in
DaoConfig (by default it is true). If isEnableInMemorySubscriptionMatching is "false", then all
subscription matching will query the database as before.
</action>
<action type="add">
Changed behaviour of FHIR Server to reject subscriptions with invalid criteria. If a Subscription
is submitted with invalid criteria, the server returns HTTP 422 "Unprocessable Entity" and the
Subscription is not persisted.
</action>
<action type="fix">
The JPA server $expunge operation could sometimes fail to expunge if
another resource linked to a resource that was being
@ -57,6 +71,16 @@
used to return a raw response. This is handy for invoking operations
that might return arbitrary binary content.
</action>
<action type="add">
Moved state and functionality out of BaseHapiFhirDao.java into new classes: LogicalReferenceHelper,
ResourceIndexedSearchParams, IdHelperService, SearcchParamExtractorService, and MatchUrlService.
</action>
<action type="add">
Replaced explicit @Bean construction in BaseConfig.java with @ComponentScan. Beans with state are annotated with
@Component and stateless beans are annotated as @Service. Also changed SearchBuilder.java and the
three Subscriber classes into @Scope("protoype") so their dependencies can be @Autowired injected
as opposed to constructor parameters.
</action>
</release>
<release version="3.6.0" date="2018-11-12" description="Food">
<action type="add">