Subscription module support (#1147)

* Reorganizing packages and dependencies to support standalone subscription running within a CDR container where all hapi modules are on the classpath.

Moved Subscription registry out of interceptor and introduced SubscriptionLoader

* Created ActiveSubscription and moved cache bits into it

* Moved ExecutorQueue stuff out into its own class

* Add test and supporting code to validate SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION behaviour

* Added SubscriptionCheckingSubscriber

* Moved a few beans to @ComponentScan

* Replaced use of beanFactory with concrete factory classes

* Switched test to use subscribablechannel

* Added SubscriptionLoaderFhirClientTest

* Confirm that our SubscriptionProviderFhirClient works with a live fhir client

* Register interceptors with DaoConfig instead of RestServer.
Also, Rename @VisibleForTesting methods with ForUnitTest

* Fix triggering service so it uses new subscriptionmatcherinterceptor

* Renamed "Database" classes to "Dao"

* processing -> matching naming change
This commit is contained in:
Ken Stevens 2018-12-18 13:09:06 -05:00 committed by GitHub
parent dc1f48ffed
commit 84a34eb3c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 3171 additions and 2125 deletions

View File

@ -0,0 +1,11 @@
# Unsupported
Most of the projects in this module are no longer supported.
The test in hapi-fhir-jpaserver-cds-example is @Ignored until Chris Schuler is able to make a change to the pom
this module depends on.
## Supported JPA Example:
The supported HAPI-FHIR JPA example is available in the [hapi-fhir-jpaserver-starter](https://github.com/hapifhir/hapi-fhir-jpaserver-starter)
project within the [hapifhir](https://github.com/hapifhir) GitHub Organization.

View File

@ -10,11 +10,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.*;
import java.io.*;
import java.net.HttpURLConnection;
@ -26,7 +22,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Scanner;
// FIXME KHS
// TODO Remove @Ignore once Chris Schuler has fixed the external jar this project depends on
@Ignore
public class CdsExampleTests {
private static IGenericClient ourClient;

View File

@ -1,16 +1,6 @@
package ca.uhn.fhir.jpa.demo;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.DaoConfig;
@ -20,13 +10,20 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.List;
public class JpaServerDemo extends RestfulServer {
@ -129,12 +126,10 @@ public class JpaServerDemo extends RestfulServer {
setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class));
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
* Register interceptors for the server based on DaoConfig.getSupportedSubscriptionTypes()
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
/*
* If you are hosting this server at a specific DNS name, the server will try to

View File

@ -1,16 +1,6 @@
package ca.uhn.fhir.jpa.demo;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.DaoConfig;
@ -20,13 +10,20 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.List;
public class JpaServerDemoDstu2 extends RestfulServer {
@ -129,12 +126,10 @@ public class JpaServerDemoDstu2 extends RestfulServer {
setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class));
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
* Register interceptors for the server based on DaoConfig.getSupportedSubscriptionTypes()
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
/*
* If you are hosting this server at a specific DNS name, the server will try to

View File

@ -1,14 +1,5 @@
package ca.uhn.fhir.jpa.demo;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
@ -16,12 +7,18 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.List;
public class JpaServerDemo extends RestfulServer {
@ -96,12 +93,10 @@ public class JpaServerDemo extends RestfulServer {
setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class));
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
* Register interceptors for the server based on DaoConfig.getSupportedSubscriptionTypes()
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
/*
* If you are hosting this server at a specific DNS name, the server will try to

View File

@ -12,6 +12,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -21,13 +22,11 @@ import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class JpaServerDemo extends RestfulServer {
@ -143,19 +142,14 @@ public class JpaServerDemo extends RestfulServer {
CorsInterceptor corsInterceptor = new CorsInterceptor();
registerInterceptor(corsInterceptor);
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
DaoConfig daoConfig = myAppCtx.getBean(DaoConfig.class);
daoConfig.setAllowExternalReferences(ContextHolder.isAllowExternalRefs());
daoConfig.setEnforceReferentialIntegrityOnDelete(!ContextHolder.isDisableReferentialIntegrity());
daoConfig.setEnforceReferentialIntegrityOnWrite(!ContextHolder.isDisableReferentialIntegrity());
daoConfig.setReuseCachedSearchResultsForMillis(ContextHolder.getReuseCachedSearchResultsForMillis());
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
}
}

View File

@ -2,18 +2,18 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.dao.DatabaseSearchParamProvider;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.subscription.config.BaseSubscriptionConfig;
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 ca.uhn.fhir.jpa.subscription.dbmatcher.CompositeInMemoryDaoSubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.dbmatcher.DaoSubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.cache.BlockingQueueSubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
@ -59,7 +59,8 @@ import javax.annotation.Nonnull;
@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)})
@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=WebSocketConfigurer.class),
@ComponentScan.Filter(type=FilterType.REGEX, pattern="ca.uhn.fhir.jpa.subscription.module.standalone.*")})
public abstract class BaseConfig implements SchedulingConfigurer {
@ -132,34 +133,29 @@ public abstract class BaseConfig implements SchedulingConfigurer {
}
@Bean
protected ISearchParamProvider searchParamProvider() {
return new DatabaseSearchParamProvider();
public InMemorySubscriptionMatcher inMemorySubscriptionMatcher() {
return new InMemorySubscriptionMatcher();
}
@Bean
public DaoSubscriptionMatcher daoSubscriptionMatcher() {
return new DaoSubscriptionMatcher();
}
/**
* 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
* Create a @Primary @Bean if you need a different implementation
*/
@Bean
@Lazy
public SubscriptionEmailInterceptor subscriptionEmailInterceptor() {
return new SubscriptionEmailInterceptor();
public ISubscriptionChannelFactory blockingQueueSubscriptionDeliveryChannelFactory() {
return new BlockingQueueSubscriptionChannelFactory();
}
@Bean
@Lazy
public SubscriptionRestHookInterceptor subscriptionRestHookInterceptor() {
return new SubscriptionRestHookInterceptor();
@Primary
public ISubscriptionMatcher subscriptionMatcherCompositeInMemoryDatabase() {
return new CompositeInMemoryDaoSubscriptionMatcher(daoSubscriptionMatcher(), inMemorySubscriptionMatcher());
}
@Bean
@Lazy
public SubscriptionWebsocketInterceptor subscriptionWebsocketInterceptor() {
return new SubscriptionWebsocketInterceptor();
}
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");

View File

@ -116,7 +116,7 @@ public class BaseDstu2Config extends BaseConfig {
@Bean
public ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryDstu2(searchParamProvider());
return new SearchParamRegistryDstu2();
}
@Bean(name = "mySystemDaoDstu2", autowire = Autowire.BY_NAME)

View File

@ -20,9 +20,7 @@ package ca.uhn.fhir.jpa.config;
* #L%
*/
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketHandler;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionWebsocketHandler;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@ -123,7 +123,7 @@ public class BaseDstu3Config extends BaseConfig {
@Bean
public ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryDstu3(searchParamProvider());
return new SearchParamRegistryDstu3();
}
@Bean(name = "mySystemDaoDstu3", autowire = Autowire.BY_NAME)

View File

@ -138,7 +138,7 @@ public class BaseR4Config extends BaseConfig {
@Bean
public ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryR4(searchParamProvider());
return new SearchParamRegistryR4();
}
@Bean(name = "mySystemDaoR4", autowire = Autowire.BY_NAME)

View File

@ -2,16 +2,16 @@ package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.jpa.dao.data.*;
import ca.uhn.fhir.jpa.dao.index.*;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
@ -59,7 +59,6 @@ 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;
@ -78,8 +77,6 @@ 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;
@ -178,13 +175,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
//@Autowired
//private ISearchResultDao mySearchResultDao;
@Autowired
private BeanFactory beanFactory;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
@Autowired
private DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer;
private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
@Autowired
private SearchBuilderFactory mySearchBuilderFactory;
private ApplicationContext myApplicationContext;
@ -746,9 +743,9 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return LogicalReferenceHelper.isLogicalReference(myConfig.getModelConfig(), theId);
}
@Override
// TODO KHS inject a searchBuilderFactory into callers of this method and delete this method
public SearchBuilder newSearchBuilder() {
return beanFactory.getBean(SearchBuilder.class, this);
return mySearchBuilderFactory.newSearchBuilder(this);
}
public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) {
@ -1406,7 +1403,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
* Indexing
*/
if (thePerformIndexing) {
myDatabaseSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams);
mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams);
}

View File

@ -24,13 +24,11 @@ import ca.uhn.fhir.context.ConfigurationException;
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.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.jpa.util.ExpungeOptions;
@ -73,17 +71,19 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
@Autowired
protected PlatformTransactionManager myPlatformTransactionManager;
@Autowired(required = false)
protected IFulltextSearchSvc mySearchDao;
@Autowired
protected DaoConfig myDaoConfig;
@Autowired
private MatchResourceUrlService myMatchResourceUrlService;
private String myResourceName;
private Class<T> myResourceType;
private String mySecondaryPrimaryKeyParamName;
@Autowired
private MatchResourceUrlService myMatchResourceUrlService;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {

View File

@ -4,12 +4,12 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.Subscription;
import org.hl7.fhir.r4.model.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -112,7 +112,7 @@ public class DaoConfig {
* update setter javadoc if default changes
*/
private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors;
private List<IServerInterceptor> myInterceptors = new ArrayList<>();
/**
* update setter javadoc if default changes
*/
@ -484,14 +484,26 @@ public class DaoConfig {
* Returns the interceptors which will be notified of operations.
*
* @see #setInterceptors(List)
* @deprecated Marked as deprecated as of HAPI 3.7.0. Use {@link #registerInterceptor} or {@link #unregisterInterceptor}instead.
*/
@Deprecated
public List<IServerInterceptor> getInterceptors() {
if (myInterceptors == null) {
myInterceptors = new ArrayList<>();
}
return myInterceptors;
}
public void registerInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
if (!myInterceptors.contains(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
@ -1462,6 +1474,47 @@ public class DaoConfig {
myModelConfig.setDefaultSearchParamsCanBeOverridden(theDefaultSearchParamsCanBeOverridden);
}
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public DaoConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) {
myModelConfig.addSupportedSubscriptionType(theSubscriptionChannelType);
return this;
}
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public Set<Subscription.SubscriptionChannelType> getSupportedSubscriptionTypes() {
return myModelConfig.getSupportedSubscriptionTypes();
}
@VisibleForTesting
public void clearSupportedSubscriptionTypesForUnitTest() {
myModelConfig.clearSupportedSubscriptionTypesForUnitTest();
}
/**
* If e-mail subscriptions are supported, the From address used when sending e-mails
*/
public String getEmailFromAddress() {
return myModelConfig.getEmailFromAddress();
}
/**
* If e-mail subscriptions are supported, the From address used when sending e-mails
*/
public void setEmailFromAddress(String theEmailFromAddress) {
myModelConfig.setEmailFromAddress(theEmailFromAddress);
}
public enum IndexEnabledEnum {
ENABLED,

View File

@ -20,17 +20,19 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
public class DatabaseSearchParamProvider implements ISearchParamProvider {
@Service
public class DaoSearchParamProvider implements ISearchParamProvider {
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired

View File

@ -0,0 +1,10 @@
package ca.uhn.fhir.jpa.dao;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Service;
@Service
public abstract class SearchBuilderFactory {
@Lookup
public abstract SearchBuilder newSearchBuilder(BaseHapiFhirDao theBaseHapiFhirResourceDao);
}

View File

@ -41,8 +41,8 @@ import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
@Service
public class DatabaseResourceLinkResolver implements IResourceLinkResolver {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DatabaseResourceLinkResolver.class);
public class DaoResourceLinkResolver implements IResourceLinkResolver {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class);
@Autowired
private DaoConfig myDaoConfig;

View File

@ -35,7 +35,7 @@ import java.util.Collection;
import java.util.List;
@Service
public class DatabaseSearchParamSynchronizer {
public class DaoSearchParamSynchronizer {
@Autowired
private DaoConfig myDaoConfig;

View File

@ -77,9 +77,9 @@ public class SearchParamWithInlineReferencesExtractor {
@Autowired
ResourceLinkExtractor myResourceLinkExtractor;
@Autowired
DatabaseResourceLinkResolver myDatabaseResourceLinkResolver;
DaoResourceLinkResolver myDaoResourceLinkResolver;
@Autowired
DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer;
DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
@ -99,7 +99,7 @@ public class SearchParamWithInlineReferencesExtractor {
extractInlineReferences(theResource);
myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDatabaseResourceLinkResolver);
myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver);
/*
* If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them
@ -258,12 +258,12 @@ public class SearchParamWithInlineReferencesExtractor {
// Store composite string uniques
if (myDaoConfig.isUniqueIndexesEnabled()) {
for (ResourceIndexedCompositeStringUnique next : myDatabaseSearchParamSynchronizer.subtract(existingParams.compositeStringUniques, theParams.compositeStringUniques)) {
for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(existingParams.compositeStringUniques, theParams.compositeStringUniques)) {
ourLog.debug("Removing unique index: {}", next);
myEntityManager.remove(next);
theEntity.getParamsCompositeStringUnique().remove(next);
}
for (ResourceIndexedCompositeStringUnique next : myDatabaseSearchParamSynchronizer.subtract(theParams.compositeStringUniques, existingParams.compositeStringUniques)) {
for (ResourceIndexedCompositeStringUnique next : myDaoSearchParamSynchronizer.subtract(theParams.compositeStringUniques, existingParams.compositeStringUniques)) {
if (myDaoConfig.isUniqueIndexesCheckedBeforeSave()) {
ResourceIndexedCompositeStringUnique existing = myResourceIndexedCompositeStringUniqueDao.findByQueryString(next.getIndexString());
if (existing != null) {

View File

@ -1,630 +0,0 @@
package ca.uhn.fhir.jpa.subscription;
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.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
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.model.dstu2.valueset.ResourceTypeEnum;
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;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.hl7.fhir.exceptions.FHIRException;
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.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;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
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";
static final String SUBSCRIPTION_TYPE = "Subscription.channel.type";
private static final Integer MAX_SUBSCRIPTION_RESULTS = 1000;
private static boolean ourForcePayloadEncodeAndDecodeForUnitTests;
private final Object myInitSubscriptionsLock = new Object();
private SubscribableChannel myProcessingChannel;
private Map<String, SubscribableChannel> myDeliveryChannel;
private ExecutorService myProcessingExecutor;
private int myExecutorThreadCount;
private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber;
private MessageHandler mySubscriptionCheckingSubscriber;
private ConcurrentHashMap<String, CanonicalSubscription> myIdToSubscription = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, SubscribableChannel> mySubscribableChannel = new ConcurrentHashMap<>();
private Multimap<String, MessageHandler> myIdToDeliveryHandler = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class);
private ThreadPoolExecutor myDeliveryExecutor;
private LinkedBlockingQueue<Runnable> myProcessingExecutorQueue;
@Autowired
private FhirContext myCtx;
@Autowired(required = false)
@Qualifier("myEventDefinitionDaoR4")
private IFhirResourceDao<org.hl7.fhir.r4.model.EventDefinition> myEventDefinitionDaoR4;
@Autowired()
private PlatformTransactionManager myTxManager;
@Autowired
@Qualifier(BaseConfig.TASK_EXECUTOR_NAME)
private AsyncTaskExecutor myAsyncTaskExecutor;
@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);
/**
* Constructor
*/
public BaseSubscriptionInterceptor() {
super();
setExecutorThreadCount(5);
}
protected CanonicalSubscription canonicalize(S theSubscription) {
switch (myCtx.getVersion().getVersion()) {
case DSTU2:
return canonicalizeDstu2(theSubscription);
case DSTU3:
return canonicalizeDstu3(theSubscription);
case R4:
return canonicalizeR4(theSubscription);
default:
throw new ConfigurationException("Subscription not supported for version: " + myCtx.getVersion().getVersion());
}
}
protected CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
try {
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
retVal.setChannelType(org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.fromCode(subscription.getChannel().getType()));
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
} catch (FHIRException theE) {
throw new InternalErrorException(theE);
}
return retVal;
}
protected CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
try {
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus().toCode()));
retVal.setChannelType(org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.fromCode(subscription.getChannel().getType().toCode()));
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
if (retVal.getChannelType() == Subscription.SubscriptionChannelType.EMAIL) {
String from;
String subjectTemplate;
String bodyTemplate;
try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
}
if (retVal.getChannelType() == Subscription.SubscriptionChannelType.RESTHOOK) {
String stripVersionIds;
String deliverLatestVersion;
try {
stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
}
} catch (FHIRException theE) {
throw new InternalErrorException(theE);
}
return retVal;
}
protected CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
retVal.setStatus(subscription.getStatus());
retVal.setChannelType(subscription.getChannel().getType());
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
if (retVal.getChannelType() == Subscription.SubscriptionChannelType.EMAIL) {
String from;
String subjectTemplate;
try {
from = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
}
if (retVal.getChannelType() == Subscription.SubscriptionChannelType.RESTHOOK) {
String stripVersionIds;
String deliverLatestVersion;
try {
stripVersionIds = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
deliverLatestVersion = subscription.getChannel().getExtensionString(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
}
List<org.hl7.fhir.r4.model.Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
if (topicExts.size() > 0) {
IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
throw new PreconditionFailedException("Topic reference must be an EventDefinition");
}
org.hl7.fhir.r4.model.EventDefinition def = myEventDefinitionDaoR4.read(ref.getReferenceElement());
retVal.addTrigger(new CanonicalSubscription.CanonicalEventDefinition(def));
}
return retVal;
}
protected SubscribableChannel createDeliveryChannel(CanonicalSubscription theSubscription) {
String subscriptionId = theSubscription.getIdElement(myCtx).getIdPart();
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("subscription-delivery-" + subscriptionId + "-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
StopWatch sw = new StopWatch();
try {
executorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
}
};
ThreadPoolExecutor deliveryExecutor = new ThreadPoolExecutor(
1,
getExecutorThreadCount(),
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
rejectedExecutionHandler);
return new ExecutorSubscribableChannel(deliveryExecutor);
}
/**
* Returns an empty handler if the interceptor will manually handle registration and unregistration
*/
protected abstract Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription);
public abstract Subscription.SubscriptionChannelType getChannelType();
protected MessageChannel getDeliveryChannel(CanonicalSubscription theSubscription) {
return mySubscribableChannel.get(theSubscription.getIdElement(myCtx).getIdPart());
}
public int getExecutorQueueSizeForUnitTests() {
return myProcessingExecutorQueue.size();
}
public int getExecutorThreadCount() {
return myExecutorThreadCount;
}
public void setExecutorThreadCount(int theExecutorThreadCount) {
Validate.inclusiveBetween(1, Integer.MAX_VALUE, theExecutorThreadCount);
myExecutorThreadCount = theExecutorThreadCount;
}
public Map<String, CanonicalSubscription> getIdToSubscription() {
return Collections.unmodifiableMap(myIdToSubscription);
}
public SubscribableChannel getProcessingChannel() {
return myProcessingChannel;
}
public void setProcessingChannel(SubscribableChannel theProcessingChannel) {
myProcessingChannel = theProcessingChannel;
}
public List<CanonicalSubscription> getRegisteredSubscriptions() {
return new ArrayList<>(myIdToSubscription.values());
}
public CanonicalSubscription hasSubscription(IIdType theId) {
Validate.notNull(theId);
Validate.notBlank(theId.getIdPart());
return myIdToSubscription.get(theId.getIdPart());
}
/**
* Read the existing subscriptions from the database
*/
@SuppressWarnings("unused")
@Scheduled(fixedDelay = 60000)
public void initSubscriptions() {
if (!myInitSubscriptionsSemaphore.tryAcquire()) {
return;
}
try {
doInitSubscriptions();
} finally {
myInitSubscriptionsSemaphore.release();
}
}
public Integer doInitSubscriptions() {
synchronized (myInitSubscriptionsLock) {
ourLog.debug("Starting init subscriptions");
SearchParameterMap map = new SearchParameterMap();
map.add(Subscription.SP_TYPE, new TokenParam(null, getChannelType().toCode()));
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
map.setLoadSynchronousUpTo(MAX_SUBSCRIPTION_RESULTS);
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao();
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.");
}
List<IBaseResource> resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size());
Set<String> allIds = new HashSet<>();
int changesCount = 0;
for (IBaseResource resource : resourceList) {
String nextId = resource.getIdElement().getIdPart();
allIds.add(nextId);
boolean changed = mySubscriptionActivatingSubscriber.activateOrRegisterSubscriptionIfRequired(resource);
if (changed) {
changesCount++;
}
}
unregisterAllSubscriptionsNotInCollection(allIds);
ourLog.trace("Finished init subscriptions - found {}", resourceList.size());
return changesCount;
}
}
@SuppressWarnings("unused")
@PreDestroy
public void preDestroy() {
getProcessingChannel().unsubscribe(mySubscriptionCheckingSubscriber);
unregisterAllSubscriptionsNotInCollection(Collections.emptyList());
}
public void registerHandler(String theSubscriptionId, MessageHandler theHandler) {
mySubscribableChannel.get(theSubscriptionId).subscribe(theHandler);
myIdToDeliveryHandler.put(theSubscriptionId, theHandler);
}
@SuppressWarnings("UnusedReturnValue")
public CanonicalSubscription registerSubscription(IIdType theId, S theSubscription) {
Validate.notNull(theId);
String subscriptionId = theId.getIdPart();
Validate.notBlank(subscriptionId);
Validate.notNull(theSubscription);
CanonicalSubscription canonicalized = canonicalize(theSubscription);
SubscribableChannel deliveryChannel = createDeliveryChannel(canonicalized);
Optional<MessageHandler> deliveryHandler = createDeliveryHandler(canonicalized);
mySubscribableChannel.put(subscriptionId, deliveryChannel);
myIdToSubscription.put(subscriptionId, canonicalized);
deliveryHandler.ifPresent(handler -> registerHandler(subscriptionId, handler));
return canonicalized;
}
protected void registerSubscriptionCheckingSubscriber() {
if (mySubscriptionCheckingSubscriber == null) {
mySubscriptionCheckingSubscriber = beanFactory.getBean(SubscriptionCheckingSubscriber.class, getChannelType(), this, mySubscriptionMatcherCompositeInMemoryDatabase);
}
getProcessingChannel().subscribe(mySubscriptionCheckingSubscriber);
}
@Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
}
@Override
public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) {
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theResource.getIdElement());
msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.DELETE);
submitResourceModified(msg);
}
@Override
public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModifiedForUpdate(theNewResource);
}
void submitResourceModifiedForUpdate(IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theNewResource.getIdElement());
msg.setOperationType(theOperationType);
msg.setNewPayload(myCtx, theNewResource);
if (ourForcePayloadEncodeAndDecodeForUnitTests) {
msg.clearPayloadDecoded();
}
submitResourceModified(msg);
}
protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) {
ourLog.trace("Registering synchronization to send resource modified message to processing channel");
/*
* We only actually submit this item work working after the
*/
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
ourLog.trace("Sending resource modified message to processing channel");
getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage));
}
});
} else {
ourLog.trace("Sending resource modified message to processing channel");
getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage));
}
}
@VisibleForTesting
public void setAsyncTaskExecutorForUnitTest(AsyncTaskExecutor theAsyncTaskExecutor) {
myAsyncTaskExecutor = theAsyncTaskExecutor;
}
public void setFhirContext(FhirContext theCtx) {
myCtx = theCtx;
}
@VisibleForTesting
public void setTxManager(PlatformTransactionManager theTxManager) {
myTxManager = theTxManager;
}
@PostConstruct
public void start() {
if (myCtx.getVersion().getVersion() == FhirVersionEnum.R4) {
Validate.notNull(myEventDefinitionDaoR4);
}
if (getProcessingChannel() == null) {
myProcessingExecutorQueue = new LinkedBlockingQueue<>(1000);
RejectedExecutionHandler rejectedExecutionHandler = (theRunnable, theExecutor) -> {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", myProcessingExecutorQueue.size());
StopWatch sw = new StopWatch();
try {
myProcessingExecutorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
};
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("subscription-proc-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
myProcessingExecutor = new ThreadPoolExecutor(
1,
getExecutorThreadCount(),
0L,
TimeUnit.MILLISECONDS,
myProcessingExecutorQueue,
threadFactory,
rejectedExecutionHandler);
setProcessingChannel(new ExecutorSubscribableChannel(myProcessingExecutor));
}
if (mySubscriptionActivatingSubscriber == null) {
IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao();
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(subscriptionDao, getChannelType(), this, myTxManager, myAsyncTaskExecutor);
}
registerSubscriptionCheckingSubscriber();
TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
initSubscriptions();
}
});
}
/**
* This is an internal API - Use with caution!
*/
public void submitResourceModified(final ResourceModifiedMessage theMsg) {
mySubscriptionActivatingSubscriber.handleMessage(theMsg.getOperationType(), theMsg.getId(myCtx), theMsg.getNewPayload(myCtx));
sendToProcessingChannel(theMsg);
}
private void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) {
for (String next : new ArrayList<>(myIdToSubscription.keySet())) {
if (!theAllIds.contains(next)) {
ourLog.info("Unregistering Subscription/{}", next);
CanonicalSubscription subscription = myIdToSubscription.get(next);
unregisterSubscription(subscription.getIdElement(myCtx));
}
}
}
public void unregisterHandler(String theSubscriptionId, MessageHandler theMessageHandler) {
SubscribableChannel channel = mySubscribableChannel.get(theSubscriptionId);
if (channel != null) {
channel.unsubscribe(theMessageHandler);
if (channel instanceof DisposableBean) {
try {
((DisposableBean) channel).destroy();
} catch (Exception e) {
ourLog.error("Failed to destroy channel bean", e);
}
}
}
mySubscribableChannel.remove(theSubscriptionId);
}
@SuppressWarnings("UnusedReturnValue")
public CanonicalSubscription unregisterSubscription(IIdType theId) {
Validate.notNull(theId);
String subscriptionId = theId.getIdPart();
Validate.notBlank(subscriptionId);
for (MessageHandler next : new ArrayList<>(myIdToDeliveryHandler.get(subscriptionId))) {
unregisterHandler(subscriptionId, next);
}
mySubscribableChannel.remove(subscriptionId);
return myIdToSubscription.remove(subscriptionId);
}
public IFhirResourceDao<?> getSubscriptionDao() {
return myDaoRegistry.getSubscriptionDao();
}
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());
}
}
@VisibleForTesting
public static void setForcePayloadEncodeAndDecodeForUnitTests(boolean theForcePayloadEncodeAndDecodeForUnitTests) {
ourForcePayloadEncodeAndDecodeForUnitTests = theForcePayloadEncodeAndDecodeForUnitTests;
}
}

View File

@ -1,98 +0,0 @@
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.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHandler;
import javax.annotation.PostConstruct;
public abstract class BaseSubscriptionSubscriber implements MessageHandler {
private final Subscription.SubscriptionChannelType myChannelType;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
@Autowired
DaoRegistry myDaoRegistry;
private IFhirResourceDao<?> mySubscriptionDao;
/**
* Constructor
*/
public BaseSubscriptionSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) {
myChannelType = theChannelType;
mySubscriptionInterceptor = theSubscriptionInterceptor;
}
@SuppressWarnings("unused") // Don't delete, used in Smile
public void setDaoRegistry(DaoRegistry theDaoRegistry) {
myDaoRegistry = theDaoRegistry;
}
@PostConstruct
public void setSubscriptionDao() {
mySubscriptionDao = myDaoRegistry.getSubscriptionDao();
}
public Subscription.SubscriptionChannelType getChannelType() {
return myChannelType;
}
public FhirContext getContext() {
return getSubscriptionDao().getContext();
}
public IFhirResourceDao getSubscriptionDao() {
return mySubscriptionDao;
}
public BaseSubscriptionInterceptor getSubscriptionInterceptor() {
return mySubscriptionInterceptor;
}
/**
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/
protected boolean subscriptionTypeApplies(CanonicalSubscription theSubscription) {
Subscription.SubscriptionChannelType channelType = getChannelType();
String subscriptionType = theSubscription.getChannelType().toCode();
return subscriptionTypeApplies(subscriptionType, channelType);
}
/**
* Does this subscription type (e.g. rest hook, websocket, etc) apply to this interceptor?
*/
static boolean subscriptionTypeApplies(String theSubscriptionChannelTypeCode, Subscription.SubscriptionChannelType theChannelType) {
boolean subscriptionTypeApplies = false;
if (theSubscriptionChannelTypeCode != null) {
if (theChannelType.toCode().equals(theSubscriptionChannelTypeCode)) {
subscriptionTypeApplies = true;
}
}
return subscriptionTypeApplies;
}
}

View File

@ -0,0 +1,32 @@
package ca.uhn.fhir.jpa.subscription;
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.subscription.module.cache.ActiveSubscription;
import ca.uhn.fhir.jpa.subscription.module.subscriber.IResourceRetriever;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
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.stereotype.Service;
@Service
public class DaoResourceRetriever implements IResourceRetriever {
private static final Logger ourLog = LoggerFactory.getLogger(ActiveSubscription.class);
@Autowired
FhirContext myFhirContext;
@Autowired
DaoRegistry myDaoRegistry;
@Override
public IBaseResource getResource(IIdType payloadId) throws ResourceGoneException {
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
return dao.read(payloadId.toVersionless());
}
}

View File

@ -0,0 +1,247 @@
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.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.config.BaseConfig;
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.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionCannonicalizer;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.SubscriptionUtil;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.Subscription;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Responsible for transitioning subscription resources from REQUESTED to ACTIVE
* Once activated, the subscription is added to the SubscriptionRegistry.
*
* Also validates criteria. If invalid, rejects the subscription without persisting the subscription.
*/
@Service
public class SubscriptionActivatingInterceptor extends ServerOperationInterceptorAdapter {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingInterceptor.class);
private static boolean ourWaitForSubscriptionActivationSynchronouslyForUnitTest;
@Autowired
private PlatformTransactionManager myTransactionManager;
@Autowired
@Qualifier(BaseConfig.TASK_EXECUTOR_NAME)
private AsyncTaskExecutor myTaskExecutor;
@Autowired
private SubscriptionRegistry mySubscriptionRegistry;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private FhirContext myFhirContext;
@Autowired
private SubscriptionCannonicalizer mySubscriptionCannonicalizer;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private DaoConfig myDaoConfig;
public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
// Grab the value for "Subscription.channel.type" so we can see if this
// subscriber applies..
String subscriptionChannelTypeCode = myFhirContext
.newTerser()
.getSingleValueOrNull(theSubscription, SubscriptionMatcherInterceptor.SUBSCRIPTION_TYPE, IPrimitiveType.class)
.getValueAsString();
Subscription.SubscriptionChannelType subscriptionChannelType = Subscription.SubscriptionChannelType.fromCode(subscriptionChannelTypeCode);
// Only activate supported subscriptions
if (!myDaoConfig.getSupportedSubscriptionTypes().contains(subscriptionChannelType)) {
return false;
}
final IPrimitiveType<?> status = myFhirContext.newTerser().getSingleValueOrNull(theSubscription, SubscriptionMatcherInterceptor.SUBSCRIPTION_STATUS, IPrimitiveType.class);
String statusString = status.getValueAsString();
final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode();
final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode();
if (requestedStatus.equals(statusString)) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
/*
* If we're in a transaction, we don't want to try and change the status from
* requested to active within the same transaction because it's too late by
* the time we get here to make modifications to the payload.
*
* So, we register a synchronization, meaning that when the transaction is
* finished, we'll schedule a task to do this in a separate worker thread
* to avoid any possibility of conflict.
*/
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
Future<?> activationFuture = myTaskExecutor.submit(new Runnable() {
@Override
public void run() {
activateSubscription(activeStatus, theSubscription, requestedStatus);
}
});
/*
* If we're running in a unit test, it's nice to be predictable in
* terms of order... In the real world it's a recipe for deadlocks
*/
if (ourWaitForSubscriptionActivationSynchronouslyForUnitTest) {
try {
activationFuture.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
ourLog.error("Failed to activate subscription", e);
}
}
}
});
return true;
} else {
return activateSubscription(activeStatus, theSubscription, requestedStatus);
}
} else if (activeStatus.equals(statusString)) {
return mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(theSubscription);
} else {
// Status isn't "active" or "requested"
return mySubscriptionRegistry.unregisterSubscriptionIfRegistered(theSubscription, statusString);
}
}
private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement());
ourLog.info("Activating subscription {} from status {} to {}", subscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus);
try {
SubscriptionUtil.setStatus(myFhirContext, subscription, theActiveStatus);
subscription = subscriptionDao.update(subscription).getResource();
submitResourceModifiedForUpdate(subscription);
return true;
} catch (final UnprocessableEntityException e) {
ourLog.info("Changing status of {} to ERROR", subscription.getIdElement());
SubscriptionUtil.setStatus(myFhirContext, subscription, "error");
SubscriptionUtil.setReason(myFhirContext, subscription, e.getMessage());
subscriptionDao.update(subscription);
return false;
}
}
void submitResourceModifiedForUpdate(IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
@Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
}
@Override
public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
}
@Override
public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType));
}
private void submitResourceModified(final ResourceModifiedMessage theMsg) {
IIdType id = theMsg.getId(myFhirContext);
if (!id.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) {
return;
}
switch (theMsg.getOperationType()) {
case DELETE:
mySubscriptionRegistry.unregisterSubscription(id);
break;
case CREATE:
case UPDATE:
final IBaseResource subscription = theMsg.getNewPayload(myFhirContext);
validateCriteria(subscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(subscription);
break;
default:
break;
}
}
public void validateCriteria(final IBaseResource theResource) {
CanonicalSubscription subscription = mySubscriptionCannonicalizer.canonicalize(theResource);
String criteria = subscription.getCriteriaString();
try {
RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, criteria);
myMatchUrlService.translateMatchUrl(criteria, resourceDef);
} catch (InvalidRequestException e) {
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage());
}
}
private void activateAndRegisterSubscriptionIfRequiredInTransaction(IBaseResource theSubscription) {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
activateOrRegisterSubscriptionIfRequired(theSubscription);
}
});
}
@VisibleForTesting
public static void setWaitForSubscriptionActivationSynchronouslyForUnitTest(boolean theWaitForSubscriptionActivationSynchronouslyForUnitTest) {
ourWaitForSubscriptionActivationSynchronouslyForUnitTest = theWaitForSubscriptionActivationSynchronouslyForUnitTest;
}
}

View File

@ -1,220 +0,0 @@
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.FhirContext;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.SubscriptionUtil;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.messaging.MessagingException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@SuppressWarnings("unchecked")
public class SubscriptionActivatingSubscriber {
private static boolean ourWaitForSubscriptionActivationSynchronouslyForUnitTest;
private final IFhirResourceDao mySubscriptionDao;
private final BaseSubscriptionInterceptor mySubscriptionInterceptor;
private final PlatformTransactionManager myTransactionManager;
private final AsyncTaskExecutor myTaskExecutor;
private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class);
private FhirContext myCtx;
private Subscription.SubscriptionChannelType myChannelType;
/**
* Constructor
*/
public SubscriptionActivatingSubscriber(IFhirResourceDao<? extends IBaseResource> theSubscriptionDao, Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, PlatformTransactionManager theTransactionManager, AsyncTaskExecutor theTaskExecutor) {
mySubscriptionDao = theSubscriptionDao;
mySubscriptionInterceptor = theSubscriptionInterceptor;
myChannelType = theChannelType;
myCtx = theSubscriptionDao.getContext();
myTransactionManager = theTransactionManager;
myTaskExecutor = theTaskExecutor;
Validate.notNull(theTaskExecutor);
}
public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) {
// Grab the value for "Subscription.channel.type" so we can see if this
// subscriber applies..
String subscriptionChannelType = myCtx
.newTerser()
.getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_TYPE, IPrimitiveType.class)
.getValueAsString();
boolean subscriptionTypeApplies = BaseSubscriptionSubscriber.subscriptionTypeApplies(subscriptionChannelType, myChannelType);
if (subscriptionTypeApplies == false) {
return false;
}
final IPrimitiveType<?> status = myCtx.newTerser().getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_STATUS, IPrimitiveType.class);
String statusString = status.getValueAsString();
final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode();
final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode();
if (requestedStatus.equals(statusString)) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
/*
* If we're in a transaction, we don't want to try and change the status from
* requested to active within the same transaction because it's too late by
* the time we get here to make modifications to the payload.
*
* So, we register a synchronization, meaning that when the transaction is
* finished, we'll schedule a task to do this in a separate worker thread
* to avoid any possibility of conflict.
*/
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
Future<?> activationFuture = myTaskExecutor.submit(new Runnable() {
@Override
public void run() {
activateSubscription(activeStatus, theSubscription, requestedStatus);
}
});
/*
* If we're running in a unit test, it's nice to be predictable in
* terms of order... In the real world it's a recipe for deadlocks
*/
if (ourWaitForSubscriptionActivationSynchronouslyForUnitTest) {
try {
activationFuture.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
ourLog.error("Failed to activate subscription", e);
}
}
}
});
return true;
} else {
return activateSubscription(activeStatus, theSubscription, requestedStatus);
}
} else if (activeStatus.equals(statusString)) {
return registerSubscriptionUnlessAlreadyRegistered(theSubscription);
} else {
// Status isn't "active" or "requested"
return unregisterSubscriptionIfRegistered(theSubscription, statusString);
}
}
protected boolean unregisterSubscriptionIfRegistered(IBaseResource theSubscription, String theStatusString) {
if (mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement()) != null) {
ourLog.info("Removing {} subscription {}", theStatusString, theSubscription.getIdElement().toUnqualified().getValue());
mySubscriptionInterceptor.unregisterSubscription(theSubscription.getIdElement());
return true;
}
return false;
}
private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) {
IBaseResource subscription = mySubscriptionDao.read(theSubscription.getIdElement());
ourLog.info("Activating subscription {} from status {} to {} for channel {}", subscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus, myChannelType);
try {
SubscriptionUtil.setStatus(myCtx, subscription, theActiveStatus);
subscription = mySubscriptionDao.update(subscription).getResource();
mySubscriptionInterceptor.submitResourceModifiedForUpdate(subscription);
return true;
} catch (final UnprocessableEntityException e) {
ourLog.info("Changing status of {} to ERROR", subscription.getIdElement());
SubscriptionUtil.setStatus(myCtx, subscription, "error");
SubscriptionUtil.setReason(myCtx, subscription, e.getMessage());
mySubscriptionDao.update(subscription);
return false;
}
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
public void handleMessage(ResourceModifiedMessage.OperationTypeEnum theOperationType, IIdType theId, final IBaseResource theSubscription) throws MessagingException {
if (!theId.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) {
return;
}
switch (theOperationType) {
case DELETE:
mySubscriptionInterceptor.unregisterSubscription(theId);
break;
case CREATE:
case UPDATE:
mySubscriptionInterceptor.validateCriteria(theSubscription);
activateAndRegisterSubscriptionIfRequiredInTransaction(theSubscription);
break;
default:
break;
}
}
private void activateAndRegisterSubscriptionIfRequiredInTransaction(IBaseResource theSubscription) {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
activateOrRegisterSubscriptionIfRequired(theSubscription);
}
});
}
protected synchronized boolean registerSubscriptionUnlessAlreadyRegistered(IBaseResource theSubscription) {
CanonicalSubscription existingSubscription = mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement());
CanonicalSubscription newSubscription = mySubscriptionInterceptor.canonicalize(theSubscription);
if (existingSubscription != null) {
if (newSubscription.equals(existingSubscription)) {
// No changes
return false;
}
}
if (existingSubscription != null) {
ourLog.info("Updating already-registered active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
mySubscriptionInterceptor.unregisterSubscription(theSubscription.getIdElement());
} else {
ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
}
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
return true;
}
@VisibleForTesting
public static void setWaitForSubscriptionActivationSynchronouslyForUnitTest(boolean theWaitForSubscriptionActivationSynchronouslyForUnitTest) {
ourWaitForSubscriptionActivationSynchronouslyForUnitTest = theWaitForSubscriptionActivationSynchronouslyForUnitTest;
}
}

View File

@ -0,0 +1,34 @@
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class SubscriptionInterceptorLoader {
@Autowired
DaoConfig myDaoConfig;
@Autowired
SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
@Autowired
SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor;
public void registerInterceptors() {
Set<Subscription.SubscriptionChannelType> supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes();
if (!supportedSubscriptionTypes.isEmpty()) {
myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor);
myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor);
}
}
@VisibleForTesting
public void unregisterInterceptorsForUnitTest() {
myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor);
myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor);
}
}

View File

@ -0,0 +1,127 @@
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.SubscriptionChannel;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionCheckingSubscriber;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/*-
* #%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%
*/
@Component
public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class);
static final String SUBSCRIPTION_STATUS = "Subscription.status";
static final String SUBSCRIPTION_TYPE = "Subscription.channel.type";
private static boolean ourForcePayloadEncodeAndDecodeForUnitTests;
private SubscribableChannel myProcessingChannel;
@Autowired
private FhirContext myFhirContext;
@Autowired
private SubscriptionCheckingSubscriber mySubscriptionCheckingSubscriber;
@Autowired
private ISubscriptionChannelFactory mySubscriptionChannelFactory;
/**
* Constructor
*/
public SubscriptionMatcherInterceptor() {
super();
}
@PostConstruct
public void start() {
if (myProcessingChannel == null) {
myProcessingChannel = mySubscriptionChannelFactory.newMatchingChannel("subscription-matching");
}
myProcessingChannel.subscribe(mySubscriptionCheckingSubscriber);
}
@SuppressWarnings("unused")
@PreDestroy
public void preDestroy() {
myProcessingChannel.unsubscribe(mySubscriptionCheckingSubscriber);
}
@Override
public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE);
}
@Override
public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) {
submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE);
}
@Override
public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) {
submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
}
private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) {
ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType);
if (ourForcePayloadEncodeAndDecodeForUnitTests) {
msg.clearPayloadDecoded();
}
submitResourceModified(msg);
}
protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) {
ourLog.trace("Sending resource modified message to processing channel");
myProcessingChannel.send(new ResourceModifiedJsonMessage(theMessage));
}
public void setFhirContext(FhirContext theCtx) {
myFhirContext = theCtx;
}
/**
* This is an internal API - Use with caution!
*/
public void submitResourceModified(final ResourceModifiedMessage theMsg) {
sendToProcessingChannel(theMsg);
}
@VisibleForTesting
public static void setForcePayloadEncodeAndDecodeForUnitTests(boolean theForcePayloadEncodeAndDecodeForUnitTests) {
ourForcePayloadEncodeAndDecodeForUnitTests = theForcePayloadEncodeAndDecodeForUnitTests;
}
@VisibleForTesting
public SubscriptionChannel getProcessingChannelForUnitTest() {
return (SubscriptionChannel) myProcessingChannel;
}
}

View File

@ -22,13 +22,16 @@ package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
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.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionChannelFactory;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.api.CacheControlDirective;
@ -38,6 +41,7 @@ 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.PreconditionFailedException;
import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.ValidateUtil;
@ -71,26 +75,31 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Service
public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc, ApplicationContextAware {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class);
public static final int DEFAULT_MAX_SUBMIT = 10000;
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class);
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
@Autowired
private FhirContext myFhirContext;
@Autowired
private DaoRegistry myDaoRegistry;
private List<BaseSubscriptionInterceptor<?>> mySubscriptionInterceptorList;
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>();
private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT;
private ApplicationContext myAppCtx;
private ExecutorService myExecutorService;
@Override
public IBaseParameters triggerSubscription(List<UriParam> theResourceIds, List<StringParam> theSearchUrls, @IdParam IIdType theSubscriptionId) {
if (mySubscriptionInterceptorList.isEmpty()) {
if (myDaoConfig.getSupportedSubscriptionTypes().isEmpty()) {
throw new PreconditionFailedException("Subscription processing not active on this server");
}
@ -293,18 +302,13 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
ourLog.info("Submitting resource {} to subscription {}", theResourceToTrigger.getIdElement().toUnqualifiedVersionless().getValue(), theSubscriptionId);
ResourceModifiedMessage msg = new ResourceModifiedMessage();
msg.setId(theResourceToTrigger.getIdElement());
msg.setOperationType(ResourceModifiedMessage.OperationTypeEnum.UPDATE);
ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, theResourceToTrigger, ResourceModifiedMessage.OperationTypeEnum.UPDATE);
msg.setSubscriptionId(new IdType(theSubscriptionId).toUnqualifiedVersionless().getValue());
msg.setNewPayload(myFhirContext, theResourceToTrigger);
return myExecutorService.submit(() -> {
for (int i = 0; ; i++) {
try {
for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) {
next.submitResourceModified(msg);
}
mySubscriptionMatcherInterceptor.submitResourceModified(msg);
break;
} catch (Exception e) {
if (i >= 3) {
@ -347,13 +351,6 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc
@SuppressWarnings("unchecked")
@PostConstruct
public void start() {
mySubscriptionInterceptorList = ObjectUtils.defaultIfNull(mySubscriptionInterceptorList, Collections.emptyList());
mySubscriptionInterceptorList = new ArrayList<>();
Collection values1 = myAppCtx.getBeansOfType(BaseSubscriptionInterceptor.class).values();
Collection<BaseSubscriptionInterceptor<?>> values = (Collection<BaseSubscriptionInterceptor<?>>) values1;
mySubscriptionInterceptorList.addAll(values);
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("SubscriptionTriggering-%d")

View File

@ -0,0 +1,35 @@
package ca.uhn.fhir.jpa.subscription.dbcache;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DaoSubscriptionProvider implements ISubscriptionProvider {
@Autowired
DaoRegistry myDaoRegistry;
@Autowired
private SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor;
@Override
public IBundleProvider search(SearchParameterMap theMap) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
return subscriptionDao.search(theMap, req);
}
@Override
public boolean loadSubscription(IBaseResource theResource) {
return mySubscriptionActivatingInterceptor.activateOrRegisterSubscriptionIfRequired(theResource);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.subscription.matcher;
package ca.uhn.fhir.jpa.subscription.dbmatcher;
/*-
* #%L
@ -21,34 +21,38 @@ package ca.uhn.fhir.jpa.subscription.matcher;
*/
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher;
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);
public class CompositeInMemoryDaoSubscriptionMatcher implements ISubscriptionMatcher {
private Logger ourLog = LoggerFactory.getLogger(CompositeInMemoryDaoSubscriptionMatcher.class);
@Autowired
SubscriptionMatcherDatabase mySubscriptionMatcherDatabase;
@Autowired
SubscriptionMatcherInMemory mySubscriptionMatcherInMemory;
private final DaoSubscriptionMatcher myDaoSubscriptionMatcher;
private final InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
@Autowired
DaoConfig myDaoConfig;
public CompositeInMemoryDaoSubscriptionMatcher(DaoSubscriptionMatcher theDaoSubscriptionMatcher, InMemorySubscriptionMatcher theInMemorySubscriptionMatcher) {
myDaoSubscriptionMatcher = theDaoSubscriptionMatcher;
myInMemorySubscriptionMatcher = theInMemorySubscriptionMatcher;
}
@Override
public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) {
SubscriptionMatchResult result;
if (myDaoConfig.isEnableInMemorySubscriptionMatching()) {
result = mySubscriptionMatcherInMemory.match(criteria, msg);
result = myInMemorySubscriptionMatcher.match(criteria, msg);
if (!result.supported()) {
ourLog.info("Criteria {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", criteria, result.getUnsupportedReason());
result = mySubscriptionMatcherDatabase.match(criteria, msg);
result = myDaoSubscriptionMatcher.match(criteria, msg);
}
} else {
result = mySubscriptionMatcherDatabase.match(criteria, msg);
result = myDaoSubscriptionMatcher.match(criteria, msg);
}
return result;
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.subscription.matcher;
package ca.uhn.fhir.jpa.subscription.dbmatcher;
/*-
* #%L
@ -24,10 +24,12 @@ 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.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -35,13 +37,9 @@ 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;
@Service
@Lazy
public class SubscriptionMatcherDatabase implements ISubscriptionMatcher {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherDatabase.class);
public class DaoSubscriptionMatcher implements ISubscriptionMatcher {
private Logger ourLog = LoggerFactory.getLogger(DaoSubscriptionMatcher.class);
@Autowired
private FhirContext myCtx;

View File

@ -1,89 +0,0 @@
package ca.uhn.fhir.jpa.subscription.email;
/*-
* #%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.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
*/
public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
/**
* This is set to autowired=false just so that implementors can supply this
* with a mechanism other than autowiring if they want
*/
@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(myBeanFactory.getBean(SubscriptionDeliveringEmailSubscriber.class, getChannelType(), this));
}
@Override
public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() {
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.EMAIL;
}
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public String getDefaultFromAddress() {
return myDefaultFromAddress;
}
/**
* The "from" address to use for any sent emails that to not explicitly specity a from address
*/
public void setDefaultFromAddress(String theDefaultFromAddress) {
Validate.notBlank(theDefaultFromAddress, "theDefaultFromAddress must not be null or blank");
myDefaultFromAddress = theDefaultFromAddress;
}
public IEmailSender getEmailSender() {
return myEmailSender;
}
/**
* Set the email sender (this method does not need to be explicitly called if you
* are using autowiring to supply the sender)
*/
public void setEmailSender(IEmailSender theEmailSender) {
myEmailSender = theEmailSender;
}
}

View File

@ -1,7 +0,0 @@
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

@ -1,46 +0,0 @@
package ca.uhn.fhir.jpa.subscription.resthook;
/*-
* #%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.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHandler;
import java.util.Optional;
public class SubscriptionRestHookInterceptor extends BaseSubscriptionInterceptor {
@Autowired
BeanFactory myBeanFactory;
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
SubscriptionDeliveringRestHookSubscriber value = myBeanFactory.getBean(SubscriptionDeliveringRestHookSubscriber.class, getChannelType(), this);
return Optional.of(value);
}
@Override
public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() {
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.RESTHOOK;
}
}

View File

@ -1,27 +0,0 @@
package ca.uhn.fhir.jpa.subscription.websocket;
/*
* #%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 org.springframework.web.socket.WebSocketHandler;
public interface ISubscriptionWebsocketHandler extends WebSocketHandler {
}

View File

@ -1,43 +0,0 @@
package ca.uhn.fhir.jpa.subscription.websocket;
/*-
* #%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.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.messaging.MessageHandler;
import java.util.Optional;
public class SubscriptionWebsocketInterceptor extends BaseSubscriptionInterceptor {
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
return Optional.empty();
}
@Override
public Subscription.SubscriptionChannelType getChannelType() {
return Subscription.SubscriptionChannelType.WEBSOCKET;
}
}

View File

@ -23,51 +23,6 @@ package ca.uhn.fhir.jpa.util;
import ca.uhn.fhir.rest.api.Constants;
public class JpaConstants {
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
/**
* This extension URL indicates whether a REST HOOK delivery should
* include the version ID when delivering.
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
public static final String EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids";
/**
* This extension URL indicates whether a REST HOOK delivery should
* reload the resource and deliver the latest version always. This
* could be useful for example if a resource which triggers a
* subscription gets updated many times in short succession and there
* is no value in delivering the older versions.
* <p>
* Note that if the resource is now deleted, this may cause
* the delivery to be cancelled altogether.
* </p>
*
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version";
/**
* Operation name for the $expunge operation
*/

View File

@ -0,0 +1,50 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionDeliveringRestHookSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import java.util.concurrent.CountDownLatch;
public class StoppableSubscriptionDeliveringRestHookSubscriber extends SubscriptionDeliveringRestHookSubscriber {
private static final Logger ourLog = LoggerFactory.getLogger(StoppableSubscriptionDeliveringRestHookSubscriber.class);
private boolean myPauseEveryMessage = false;
private CountDownLatch myCountDownLatch;
@Override
public void handleMessage(Message theMessage) throws MessagingException {
if (myCountDownLatch != null) {
myCountDownLatch.countDown();
}
if (myPauseEveryMessage) {
waitIfPaused();
}
super.handleMessage(theMessage);
}
private synchronized void waitIfPaused() {
try {
if (myPauseEveryMessage) {
wait();
}
} catch (InterruptedException theE) {
ourLog.error("interrupted", theE);
}
}
public void pause() {
myPauseEveryMessage = true;
}
public synchronized void unPause() {
myPauseEveryMessage = false;
notifyAll();
}
public void setCountDownLatch(CountDownLatch theCountDownLatch) {
myCountDownLatch = theCountDownLatch;
}
}

View File

@ -1,28 +1,23 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.subscription.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.JavaMailEmailSender;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.junit.Assert.fail;
@Configuration
@Import(TestJPAConfig.class)

View File

@ -2,12 +2,12 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionDeliveringRestHookSubscriber;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
@ -38,4 +38,16 @@ public class TestJPAConfig {
public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) {
return new UnregisterScheduledProcessor(theEnv);
}
@Lazy
@Bean
public SubscriptionTestUtil subscriptionTestUtil() {
return new SubscriptionTestUtil();
}
@Bean
@Primary
public SubscriptionDeliveringRestHookSubscriber stoppableSubscriptionDeliveringRestHookSubscriber() {
return new StoppableSubscriptionDeliveringRestHookSubscriber();
}
}

View File

@ -1,27 +1,21 @@
package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.fail;
@ -102,8 +96,8 @@ public class TestR4Config extends BaseJavaConfigR4 {
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)
.logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
// .logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
// .countQuery(new ThreadQueryCountHolder())
.countQuery(singleQueryCountHolder())
.build();

View File

@ -16,6 +16,7 @@ import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
@ -181,6 +182,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
protected IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt> myValueSetDao;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
protected SubscriptionLoader mySubscriptionLoader;
@Before
public void beforeCreateInterceptor() {

View File

@ -2,8 +2,7 @@ package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingSubscriber;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.model.Subscription;
@ -23,19 +22,20 @@ import static org.junit.Assert.*;
public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Test {
@Autowired
private SubscriptionRestHookInterceptor myInterceptor;
private DaoConfig myDaoConfig;
@After
public void afterResetDao() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
}
@Before
public void before() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
}
@Test
@ -83,8 +83,6 @@ public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Tes
});
myEntityManager.clear();
myInterceptor.start();
}
/**
@ -104,9 +102,6 @@ public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Tes
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
@ -124,9 +119,6 @@ public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Tes
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
@ -145,9 +137,6 @@ public class FhirResourceDaoDstu3InvalidSubscriptionTest extends BaseJpaDstu3Tes
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
@AfterClass

View File

@ -15,7 +15,7 @@ import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
@ -223,8 +223,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
@Qualifier("mySearchParameterDaoR4")
protected IFhirResourceDao<SearchParameter> mySearchParameterDao;
@Autowired
protected ISearchParamPresenceSvc mySearchParamPresenceSvc;
@Autowired
protected ISearchParamRegistry mySearchParamRegsitry;
@Autowired
protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc;
@ -273,6 +271,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
protected ICacheWarmingSvc myCacheWarmingSvc;
@Autowired
private JpaValidationSupportChainR4 myJpaValidationSupportChainR4;
@Autowired
protected SubscriptionRegistry mySubscriptionRegistry;
@After()
public void afterCleanupDao() {

View File

@ -2,8 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingSubscriber;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IIdType;
@ -12,7 +11,6 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@ -23,19 +21,16 @@ import static org.junit.Assert.*;
public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
@Autowired
private SubscriptionRestHookInterceptor myInterceptor;
@After
public void afterResetDao() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
}
@Before
public void before() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
}
@Test
@ -83,8 +78,6 @@ public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
});
myEntityManager.clear();
myInterceptor.start();
}
/**
@ -104,9 +97,6 @@ public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
@ -124,9 +114,6 @@ public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
IIdType id = mySubscriptionDao.create(s).getId().toUnqualifiedVersionless();
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
/**
@ -145,9 +132,6 @@ public class FhirResourceDaoR4InvalidSubscriptionTest extends BaseJpaR4Test {
assertNotNull(id.getIdPart());
BaseHapiFhirDao.setValidationDisabledForUnitTest(false);
myInterceptor.start();
}
@AfterClass

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport;
@ -89,6 +90,11 @@ public class SearchParamExtractorR4Test {
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return null;
}
@Override
public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) {
// nothing
}
};
}

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry;
@ -46,7 +45,6 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
protected static Server ourServer;
protected static String ourServerBase;
protected static GenericWebApplicationContext ourWebApplicationContext;
protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor;
protected static DatabaseBackedPagingProvider ourPagingProvider;
protected static PlatformTransactionManager ourTxManager;
protected static Integer ourConnectionPoolSize;
@ -101,7 +99,6 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
ourWebApplicationContext.setParent(myAppCtx);
ourWebApplicationContext.refresh();
ourRestHookSubscriptionInterceptor = ourWebApplicationContext.getBean(SubscriptionRestHookInterceptor.class);
ourTxManager = ourWebApplicationContext.getBean(PlatformTransactionManager.class);
proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext);

View File

@ -7,8 +7,6 @@ import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu3;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.parser.StrictErrorHandler;
@ -60,8 +58,6 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
protected static GenericWebApplicationContext ourWebApplicationContext;
protected static SearchParamRegistryDstu3 ourSearchParamRegistry;
protected static DatabaseBackedPagingProvider ourPagingProvider;
protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor;
protected static SubscriptionEmailInterceptor ourEmailSubscriptionInterceptor;
protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
private static Server ourServer;
@ -158,8 +154,6 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
myValidationSupport = wac.getBean(JpaValidationSupportChainDstu3.class);
mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class);
mySearchEntityDao = wac.getBean(ISearchDao.class);
ourRestHookSubscriptionInterceptor = wac.getBean(SubscriptionRestHookInterceptor.class);
ourEmailSubscriptionInterceptor = wac.getBean(SubscriptionEmailInterceptor.class);
ourSearchParamRegistry = wac.getBean(SearchParamRegistryDstu3.class);
ourSubscriptionTriggeringProvider = wac.getBean(SubscriptionTriggeringProvider.class);

View File

@ -3,10 +3,11 @@ package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR4;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.subscription.resthook.SubscriptionRestHookInterceptor;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryR4;
import ca.uhn.fhir.jpa.subscription.SubscriptionMatcherInterceptor;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainR4;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
@ -33,6 +34,7 @@ import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
@ -61,7 +63,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected static ISearchDao mySearchEntityDao;
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
protected static GenericWebApplicationContext ourWebApplicationContext;
protected static SubscriptionRestHookInterceptor ourReskHookSubscriptionInterceptor;
protected static SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor;
private static Server ourServer;
protected IGenericClient ourClient;
protected ResourceCountCache ourResourceCountsCache;
@ -69,6 +71,9 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
private Object ourGraphQLProvider;
private boolean ourRestHookSubscriptionInterceptorRequested;
@Autowired
protected SubscriptionLoader mySubscriptionLoader;
public BaseResourceProviderR4Test() {
super();
}
@ -156,7 +161,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
mySearchCoordinatorSvc = wac.getBean(ISearchCoordinatorSvc.class);
mySearchEntityDao = wac.getBean(ISearchDao.class);
ourSearchParamRegistry = wac.getBean(SearchParamRegistryR4.class);
ourReskHookSubscriptionInterceptor = wac.getBean(SubscriptionRestHookInterceptor.class);
ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class);
myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000);
@ -177,19 +182,6 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
}
}
/**
* This is lazy created so we only ask for it if its needed
*/
protected SubscriptionRestHookInterceptor getRestHookSubscriptionInterceptor() {
SubscriptionRestHookInterceptor retVal = ourWebApplicationContext.getBean(SubscriptionRestHookInterceptor.class);
ourRestHookSubscriptionInterceptorRequested = true;
return retVal;
}
protected boolean hasRestHookSubscriptionInterceptor() {
return ourRestHookSubscriptionInterceptorRequested;
}
protected boolean shouldLogClient() {
return true;
}
@ -206,21 +198,20 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
return names;
}
protected void waitForRegisteredSubscriptionCount(int theSize) throws Exception {
protected void waitForActivatedSubscriptionCount(int theSize) throws Exception {
for (int i = 0; ; i++) {
if (i == 10) {
fail("Failed to init subscriptions");
}
try {
getRestHookSubscriptionInterceptor().doInitSubscriptions();
mySubscriptionLoader.initSubscriptions();
break;
} catch (ResourceVersionConflictException e) {
Thread.sleep(250);
}
}
SubscriptionRestHookInterceptor interceptor = getRestHookSubscriptionInterceptor();
TestUtil.waitForSize(theSize, () -> interceptor.getRegisteredSubscriptions().size());
TestUtil.waitForSize(theSize, () -> mySubscriptionRegistry.size());
Thread.sleep(500);
}

View File

@ -19,7 +19,6 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
@ -38,7 +37,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;

View File

@ -1,11 +1,9 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test;
import ca.uhn.fhir.jpa.subscription.module.SubscriptionChannel;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
@ -26,7 +24,7 @@ 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 org.springframework.messaging.SubscribableChannel;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
@ -39,7 +37,6 @@ import java.util.List;
public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR4Test.class);
private static int ourListenerPort;
private static RestfulServer ourListenerRestServer;
private static Server ourListenerServer;
@ -50,9 +47,9 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
@Autowired
private SingleQueryCountHolder myCountHolder;
@Autowired
protected DaoConfig myDaoConfig;
protected SubscriptionTestUtil mySubscriptionTestUtil;
@Autowired
private DaoRegistry myDaoRegistry;
protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
protected CountingInterceptor myCountingInterceptor;
@ -65,7 +62,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
@After
public void afterUnregisterRestHookListener() {
BaseSubscriptionInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(false);
SubscriptionMatcherInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(false);
for (IIdType next : mySubscriptionIds) {
IIdType nextId = next.toUnqualifiedVersionless();
@ -81,12 +78,12 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterRestHookListener() {
ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
@ -101,12 +98,12 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
for (IBaseResource next : BundleUtil.toListOfResources(myFhirCtx, allSubscriptions)) {
ourClient.delete().resource(next).execute();
}
waitForRegisteredSubscriptionCount(0);
waitForActivatedSubscriptionCount(0);
ExecutorSubscribableChannel processingChannel = (ExecutorSubscribableChannel) getRestHookSubscriptionInterceptor().getProcessingChannel();
processingChannel.setInterceptors(new ArrayList<>());
SubscriptionChannel processingChannel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest();
processingChannel.clearInterceptorsForUnitTest();
myCountingInterceptor = new CountingInterceptor();
processingChannel.addInterceptor(myCountingInterceptor);
processingChannel.addInterceptorForUnitTest(myCountingInterceptor);
}
@ -135,7 +132,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
protected void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.waitForQueueToDrain();
}
@PostConstruct
@ -156,8 +153,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute();
String observationId = methodOutcome.getId().getIdPart();
observation.setId(observationId);
observation.setId(methodOutcome.getId());
return observation;
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;

View File

@ -1,10 +1,8 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider;
import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSearchParamProvider;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Enumerations;
@ -20,18 +18,18 @@ import static org.junit.Assert.assertEquals;
public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test {
@Autowired
BaseSearchParamRegistry mySearchParamRegistry;
ISearchParamRegistry mySearchParamRegistry;
@Autowired
ISearchParamProvider origSearchParamProvider;
@Before
public void useFhirClientSearchParamProvider() {
mySearchParamRegistry.setSearchParamProvider(new FhirClientSearchParamProvider(ourClient));
mySearchParamRegistry.setSearchParamProviderForUnitTest(new FhirClientSearchParamProvider(ourClient));
}
@After
public void revert() {
mySearchParamRegistry.setSearchParamProvider(origSearchParamProvider);
mySearchParamRegistry.setSearchParamProviderForUnitTest(origSearchParamProvider);
}
@Test
@ -48,7 +46,7 @@ public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test {
mySearchParameterDao.create(sp);
mySearchParamRegsitry.forceRefresh();
createSubscription(criteria, "application/json");
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
{
Observation observation = new Observation();
@ -81,8 +79,5 @@ public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test {
waitForQueueToDrain();
waitForSize(2, ourUpdatedObservations);
}
}
}

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader;
import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSubscriptionProvider;
import ca.uhn.fhir.rest.api.Constants;
import org.hl7.fhir.dstu3.model.Subscription;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class FhirClientSubscriptionProviderTest extends BaseSubscriptionsR4Test {
@Autowired
SubscriptionLoader mySubscriptionLoader;
@Autowired
ISubscriptionProvider origSubscriptionProvider;
@Autowired
AutowireCapableBeanFactory autowireCapableBeanFactory;
@Before
public void useFhirClientSubscriptionProvider() {
FhirClientSubscriptionProvider subscriptionProvider = new FhirClientSubscriptionProvider(ourClient);
// This bean is only available in the standalone subscription context, so we have to autowire it manually.
autowireCapableBeanFactory.autowireBean(subscriptionProvider);
mySubscriptionLoader.setSubscriptionProviderForUnitTest(subscriptionProvider);
}
@After
public void revert() {
mySubscriptionLoader.setSubscriptionProviderForUnitTest(origSubscriptionProvider);
}
private String myCode = "1000000050";
@Test
public void testSubscriptionLoaderFhirClient() throws Exception {
String payload = "application/fhir+json";
String criteria1 = "Observation?code=SNOMED-CT|" + myCode + "&_format=xml";
String criteria2 = "Observation?code=SNOMED-CT|" + myCode + "111&_format=xml";
List<Subscription> subs = new ArrayList<>();
createSubscription(criteria1, payload);
createSubscription(criteria2, payload);
waitForActivatedSubscriptionCount(2);
sendObservation(myCode, "SNOMED-CT");
waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations);
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
}
}

View File

@ -1,10 +1,11 @@
package ca.uhn.fhir.jpa.subscription.r4;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.instance.model.api.*;
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
public class FhirR4Util {

View File

@ -0,0 +1,86 @@
package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.subscription.module.LinkedBlockingQueueSubscriptionChannel;
import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.SubscriptionDeliveringEmailSubscriber;
import org.hl7.fhir.instance.model.Subscription;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
public class SubscriptionTestUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionTestUtil.class);
private JavaMailEmailSender myEmailSender;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private SubscriptionInterceptorLoader mySubscriptionInterceptorLoader;
@Autowired
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
@Autowired
private SubscriptionRegistry mySubscriptionRegistry;
public int getExecutorQueueSize() {
LinkedBlockingQueueSubscriptionChannel channel = (LinkedBlockingQueueSubscriptionChannel) mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest();
return channel.getQueueSizeForUnitTest();
}
// TODO KHS replace this and similar functions with CountdownLatch
public void waitForQueueToDrain() throws InterruptedException {
Thread.sleep(100);
ourLog.info("Executor work queue has {} items", getExecutorQueueSize());
if (getExecutorQueueSize() > 0) {
while (getExecutorQueueSize() > 0) {
Thread.sleep(50);
}
ourLog.info("Executor work queue has {} items", getExecutorQueueSize());
}
Thread.sleep(100);
}
public void registerEmailInterceptor() {
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.EMAIL);
mySubscriptionInterceptorLoader.registerInterceptors();
}
public void registerRestHookInterceptor() {
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.RESTHOOK);
mySubscriptionInterceptorLoader.registerInterceptors();
}
public void registerWebSocketInterceptor() {
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.WEBSOCKET);
mySubscriptionInterceptorLoader.registerInterceptors();
}
public void unregisterSubscriptionInterceptor() {
myDaoConfig.clearSupportedSubscriptionTypesForUnitTest();
mySubscriptionInterceptorLoader.unregisterInterceptorsForUnitTest();
}
public int getExecutorQueueSizeForUnitTests() {
return getExecutorQueueSize();
}
public void initEmailSender(int theListenerPort) {
myEmailSender = new JavaMailEmailSender();
myEmailSender.setSmtpServerHostname("localhost");
myEmailSender.setSmtpServerPort(theListenerPort);
myEmailSender.start();
}
public void setEmailSender(IIdType theIdElement) {
ActiveSubscription activeSubscription = mySubscriptionRegistry.get(theIdElement.getIdPart());
SubscriptionDeliveringEmailSubscriber subscriber = (SubscriptionDeliveringEmailSubscriber) activeSubscription.getDeliveryHandlerForUnitTest();
subscriber.setEmailSender(myEmailSender);
}
public IEmailSender getEmailSender() {
return myEmailSender;
}
}

View File

@ -1,10 +1,7 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.subscription.email.SubscriptionEmailInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
@ -22,8 +19,6 @@ import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.AsyncTaskExecutor;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
@ -42,12 +37,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
private List<IIdType> mySubscriptionIds = new ArrayList<>();
@Autowired
private SubscriptionEmailInterceptor mySubscriber;
@Autowired
private List<IFhirResourceDao<?>> myResourceDaos;
@Autowired
@Qualifier(BaseConfig.TASK_EXECUTOR_NAME)
private AsyncTaskExecutor myAsyncTaskExecutor;
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void after() throws Exception {
@ -58,38 +48,16 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
ourClient.delete().resourceById(next).execute();
}
mySubscriptionIds.clear();
ourRestServer.unregisterInterceptor(mySubscriber);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void before() throws Exception {
super.before();
JavaMailEmailSender emailSender = new JavaMailEmailSender();
emailSender.setSmtpServerHostname("localhost");
emailSender.setSmtpServerPort(ourListenerPort);
emailSender.start();
mySubscriptionTestUtil.initEmailSender(ourListenerPort);
mySubscriber.setEmailSender(emailSender);
mySubscriber.setResourceDaos(myResourceDaos);
mySubscriber.setFhirContext(myFhirCtx);
mySubscriber.setTxManager(ourTxManager);
mySubscriber.setAsyncTaskExecutorForUnitTest(myAsyncTaskExecutor);
mySubscriber.start();
ourRestServer.registerInterceptor(mySubscriber);
// ourLog.info("Sending test email to warm up the server");
// EmailDetails details = new EmailDetails();
// details.setFrom("a@a.com");
// details.setTo(Arrays.asList("b@b.com"));
// details.setSubjectTemplate("SUBJ");
// details.setBodyTemplate("BODY");
// emailSender.send(details);
// ourLog.info("Done sending test email to warm up the server");
// Store store = ourTestSmtp.getManagers().getImapHostManager().getStore();
// MailFolder mailbox = store.getMailbox(ImapConstants.USER_NAMESPACE);
// mailbox.deleteAllMessages();
mySubscriptionTestUtil.registerEmailInterceptor();
}
private Subscription createSubscription(String criteria, String payload, String endpoint) throws InterruptedException {
@ -108,7 +76,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
subscription.setId(methodOutcome.getId().getIdPart());
mySubscriptionIds.add(methodOutcome.getId());
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
return subscription;
}
@ -138,8 +106,8 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test {
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription subscription1 = createSubscription(criteria1, payload, "to1@example.com,to2@example.com");
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
mySubscriptionTestUtil.setEmailSender(subscription1.getIdElement());
assertEquals(0, Arrays.asList(ourTestSmtp.getReceivedMessages()).size());
Observation observation1 = sendObservation(code, "SNOMED-CT");

View File

@ -2,9 +2,9 @@ package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import com.google.common.collect.Lists;
import com.icegreen.greenmail.store.FolderException;
@ -13,6 +13,7 @@ import com.icegreen.greenmail.util.ServerSetup;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
@ -21,7 +22,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
/**
* Test the rest-hook subscriptions
@ -29,6 +30,10 @@ import static org.junit.Assert.*;
public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(EmailSubscriptionDstu3Test.class);
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
private static List<Observation> ourCreatedObservations = Lists.newArrayList();
private static int ourListenerPort;
private static List<String> ourContentTypes = new ArrayList<>();
@ -51,23 +56,17 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(ourEmailSubscriptionInterceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterEmailListener() throws FolderException {
ourTestSmtp.purgeEmailFromAllMailboxes();
;
ourRestServer.registerInterceptor(ourEmailSubscriptionInterceptor);
mySubscriptionTestUtil.registerEmailInterceptor();
JavaMailEmailSender emailSender = new JavaMailEmailSender();
emailSender.setSmtpServerHostname("localhost");
emailSender.setSmtpServerPort(ourListenerPort);
emailSender.start();
mySubscriptionTestUtil.initEmailSender(ourListenerPort);
ourEmailSubscriptionInterceptor.setEmailSender(emailSender);
ourEmailSubscriptionInterceptor.setDefaultFromAddress("123@hapifhir.io");
myDaoConfig.setEmailFromAddress("123@hapifhir.io");
}
private Subscription createSubscription(String theCriteria, String thePayload) throws InterruptedException {
@ -115,8 +114,9 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
createSubscription(criteria1, payload);
Subscription subscription = createSubscription(criteria1, payload);
waitForQueueToDrain();
mySubscriptionTestUtil.setEmailSender(subscription.getIdElement());
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
@ -150,19 +150,18 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("mailto:myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(subscriptionTemp));
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
waitForQueueToDrain();
mySubscriptionTestUtil.setEmailSender(subscriptionTemp.getIdElement());
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
@ -197,10 +196,10 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
Subscription subscriptionTemp = ourClient.read(Subscription.class, sub1.getId());
Assert.assertNotNull(subscriptionTemp);
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM)
.setValue(new StringType("myfrom@from.com"));
subscriptionTemp.getChannel().addExtension()
.setUrl(JpaConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setUrl(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE)
.setValue(new StringType("This is a subject"));
subscriptionTemp.setIdElement(subscriptionTemp.getIdElement().toUnqualifiedVersionless());
@ -208,6 +207,7 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
ourLog.info("Subscription ID is: {}", id.getValue());
waitForQueueToDrain();
mySubscriptionTestUtil.setEmailSender(subscriptionTemp.getIdElement());
sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
@ -238,7 +238,7 @@ public class EmailSubscriptionDstu3Test extends BaseResourceProviderDstu3Test {
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourEmailSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}
@AfterClass

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.jpa.subscription.email;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.EmailDetails;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.JavaMailEmailSender;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
@ -15,7 +17,7 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Arrays;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
public class JavaMailEmailSenderTest {

View File

@ -1,10 +1,10 @@
package ca.uhn.fhir.jpa.subscription.matcher;
package ca.uhn.fhir.jpa.subscription.module.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
@ -29,18 +29,18 @@ import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestR4Config.class})
public class SubscriptionMatcherInMemoryTestR4 {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionMatcherInMemoryTestR4.class);
public class InMemorySubscriptionMatcherTestR4 {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(InMemorySubscriptionMatcherTestR4.class);
@Autowired
SubscriptionMatcherInMemory mySubscriptionMatcherInMemory;
InMemorySubscriptionMatcher myInMemorySubscriptionMatcher;
@Autowired
FhirContext myContext;
private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) {
String criteria = params.toNormalizedQueryString(myContext);
ourLog.info("Criteria: <{}>", criteria);
return mySubscriptionMatcherInMemory.match(criteria, resource);
return myInMemorySubscriptionMatcher.match(criteria, resource);
}
private void assertUnsupported(IBaseResource resource, SearchParameterMap params) {
@ -380,18 +380,16 @@ public class SubscriptionMatcherInMemoryTestR4 {
params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam"));
try {
String criteria = params.toNormalizedQueryString(myContext);
ResourceModifiedMessage msg = new ResourceModifiedMessage();
ResourceModifiedMessage msg = new ResourceModifiedMessage(myContext, patient, ResourceModifiedMessage.OperationTypeEnum.CREATE);
msg.setSubscriptionId("Subscription/123");
msg.setId(new IdType("Patient/ABC"));
msg.setNewPayload(myContext, patient);
SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, msg);
SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, msg);
fail();
} catch (InternalErrorException e){
assertEquals("Failure processing resource ID[Patient/ABC] for subscription ID[Subscription/123]: Invalid resource reference found at path[Patient.managingOrganization] - Does not contain resource type - urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage());
}
}
@Test
public void testSearchResourceReferenceOnlyCorrectPath() {
Organization org = new Organization();

View File

@ -1,9 +1,9 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingSubscriber;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.Constants;
@ -20,6 +20,7 @@ import org.hl7.fhir.r4.model.*;
import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
@ -28,7 +29,6 @@ import java.util.Enumeration;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourceProviderR4Test {
@ -41,20 +41,23 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc
private static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
private static List<String> ourHeaders = Collections.synchronizedList(new ArrayList<>());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void afterResetSubscriptionActivatingInterceptor() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
}
@After
public void afterUnregisterRestHookListener() {
ourRestServer.unregisterInterceptor(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeSetSubscriptionActivatingInterceptor() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
getRestHookSubscriptionInterceptor().initSubscriptions();
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
mySubscriptionLoader.initSubscriptions();
}
@ -105,11 +108,8 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc
createSubscription(criteria1, payload, ourListenerServerBase);
createSubscription(criteria2, payload, ourListenerServerBase);
ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor());
getRestHookSubscriptionInterceptor().initSubscriptions();
assertTrue(hasRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.registerRestHookInterceptor();
mySubscriptionLoader.initSubscriptions();
sendObservation(code, "SNOMED-CT");
@ -120,9 +120,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc
}
private void waitForQueueToDrain() throws InterruptedException {
if (hasRestHookSubscriptionInterceptor()) {
RestHookTestDstu2Test.waitForQueueToDrain(getRestHookSubscriptionInterceptor());
}
mySubscriptionTestUtil.waitForQueueToDrain();
}
public static class ObservationListener implements IResourceProvider {

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Observation;
@ -27,11 +28,13 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* Test the rest-hook subscriptions
@ -47,6 +50,9 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
private static List<String> ourUpdatedObservations = Lists.newArrayList();
private List<IIdType> mySubscriptionIds = new ArrayList<IIdType>();
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void afterUnregisterRestHookListener() {
ourLog.info("** AFTER **");
@ -62,12 +68,12 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterRestHookListener() {
ourRestServer.registerInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
@ -279,7 +285,7 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}
@BeforeClass
@ -309,18 +315,6 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test {
ourListenerServer.stop();
}
public static void waitForQueueToDrain(BaseSubscriptionInterceptor theSubscriptionInterceptor) throws InterruptedException {
Thread.sleep(100);
ourLog.info("Executor work queue has {} items", theSubscriptionInterceptor.getExecutorQueueSizeForUnitTests());
if (theSubscriptionInterceptor.getExecutorQueueSizeForUnitTests() > 0) {
while (theSubscriptionInterceptor.getExecutorQueueSizeForUnitTests() > 0) {
Thread.sleep(50);
}
ourLog.info("Executor work queue has {} items", theSubscriptionInterceptor.getExecutorQueueSizeForUnitTests());
}
Thread.sleep(100);
}
public static class ObservationListener implements IResourceProvider {
@Create

View File

@ -1,9 +1,10 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
@ -21,6 +22,7 @@ import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
@ -45,6 +47,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
private static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void afterUnregisterRestHookListener() {
ourLog.info("**** Starting @After *****");
@ -61,13 +66,12 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterRestHookListener() {
ourRestServer.registerInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
@ -346,7 +350,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test {
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}
@BeforeClass

View File

@ -1,42 +1,26 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.DaoRegistry;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.RestHookTestDstu2Test;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.jpa.config.StoppableSubscriptionDeliveringRestHookSubscriber;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionMatcherInterceptor;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
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 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;
import org.hl7.fhir.instance.model.api.IBaseBundle;
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.junit.After;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
@ -46,7 +30,16 @@ import static org.junit.Assert.*;
* Test the rest-hook subscriptions
*/
public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestR4Test.class);
private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR4Test.class);
@Autowired
StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber;
@After
public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() {
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null);
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
}
@Test
public void testRestHookSubscriptionApplicationFhirJson() throws Exception {
@ -58,7 +51,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
createSubscription(criteria1, payload);
createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
sendObservation(code, "SNOMED-CT");
@ -75,10 +68,10 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
String payload = "application/fhir+json";
createSubscription(criteria, payload);
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
for (int i = 0; i < 5; i++) {
Integer changes = ourReskHookSubscriptionInterceptor.doInitSubscriptions();
assertEquals(0, changes.intValue());
int changes = this.mySubscriptionLoader.doInitSubscriptionsForUnitTest();
assertEquals(0, changes);
}
}
@ -92,7 +85,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
createSubscription(criteria1, payload);
createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
Observation obs = sendObservation(code, "SNOMED-CT");
@ -122,32 +115,137 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
waitForRegisteredSubscriptionCount(0);
waitForActivatedSubscriptionCount(0);
Subscription subscription1 = createSubscription(criteria1, payload);
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
int modCount = myCountingInterceptor.getSentCount();
subscription1
.getChannel()
.addExtension(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
subscription1
.getChannel()
.addExtension(JpaConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
ourLog.info("** About to update subscription");
ourClient.update().resource(subscription1).execute();
waitForSize(modCount + 1, () -> myCountingInterceptor.getSentCount());
ourLog.info("** About to send observation");
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Should see 1 subscription notification
waitForQueueToDrain();
waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations);
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0));
assertEquals(observation1.getIdElement().getIdPart(), ourUpdatedObservations.get(0).getIdElement().getIdPart());
assertEquals(null, ourUpdatedObservations.get(0).getIdElement().getVersionIdPart());
IdType idElement = ourUpdatedObservations.get(0).getIdElement();
assertEquals(observation1.getIdElement().getIdPart(), idElement.getIdPart());
// VersionId is present
assertEquals(observation1.getIdElement().getVersionIdPart(), idElement.getVersionIdPart());
subscription1
.getChannel()
.addExtension(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true"));
ourLog.info("** About to update subscription");
int modCount = myCountingInterceptor.getSentCount();
ourClient.update().resource(subscription1).execute();
waitForSize(modCount + 1, () -> myCountingInterceptor.getSentCount());
ourLog.info("** About to send observation");
Observation observation2 = sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
waitForSize(0, ourCreatedObservations);
waitForSize(2, ourUpdatedObservations);
assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(1));
idElement = ourUpdatedObservations.get(1).getIdElement();
assertEquals(observation2.getIdElement().getIdPart(), idElement.getIdPart());
// Now VersionId is stripped
assertEquals(null, idElement.getVersionIdPart());
}
@Test
public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception {
String payload = "application/json";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
waitForActivatedSubscriptionCount(0);
createSubscription(criteria1, payload);
waitForActivatedSubscriptionCount(1);
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
final CountDownLatch countDownLatch = new CountDownLatch(1);
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
ourLog.info("** About to send observation");
Observation observation = sendObservation(code, "SNOMED-CT");
assertEquals("1", observation.getIdElement().getVersionIdPart());
assertNull(observation.getComment());
observation.setComment("changed");
MethodOutcome methodOutcome = ourClient.update().resource(observation).execute();
assertEquals("2", methodOutcome.getId().getVersionIdPart());
assertEquals("changed", observation.getComment());
// Wait for our two delivery channel threads to be paused
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
// Open the floodgates!
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
waitForSize(0, ourCreatedObservations);
waitForSize(2, ourUpdatedObservations);
Observation observation1 = ourUpdatedObservations.get(0);
Observation observation2 = ourUpdatedObservations.get(1);
assertEquals("1", observation1.getIdElement().getVersionIdPart());
assertNull(observation1.getComment());
assertEquals("2", observation2.getIdElement().getVersionIdPart());
assertEquals("changed", observation2.getComment());
}
@Test
public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception {
String payload = "application/json";
String code = "1000000050";
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
waitForActivatedSubscriptionCount(0);
Subscription subscription = newSubscription(criteria1, payload);
subscription
.getChannel()
.addExtension(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true"));
ourClient.create().resource(subscription).execute();
waitForActivatedSubscriptionCount(1);
myStoppableSubscriptionDeliveringRestHookSubscriber.pause();
final CountDownLatch countDownLatch = new CountDownLatch(1);
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
ourLog.info("** About to send observation");
Observation observation = sendObservation(code, "SNOMED-CT");
assertEquals("1", observation.getIdElement().getVersionIdPart());
assertNull(observation.getComment());
observation.setComment("changed");
MethodOutcome methodOutcome = ourClient.update().resource(observation).execute();
assertEquals("2", methodOutcome.getId().getVersionIdPart());
assertEquals("changed", observation.getComment());
// Wait for our two delivery channel threads to be paused
assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS));
// Open the floodgates!
myStoppableSubscriptionDeliveringRestHookSubscriber.unPause();
waitForSize(0, ourCreatedObservations);
waitForSize(2, ourUpdatedObservations);
Observation observation1 = ourUpdatedObservations.get(0);
Observation observation2 = ourUpdatedObservations.get(1);
assertEquals("2", observation1.getIdElement().getVersionIdPart());
assertEquals("changed", observation1.getComment());
assertEquals("2", observation2.getIdElement().getVersionIdPart());
assertEquals("changed", observation2.getComment());
}
@Test
@ -160,7 +258,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription1 = createSubscription(criteria1, payload);
Subscription subscription2 = createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
Observation observation1 = sendObservation(code, "SNOMED-CT");
@ -240,7 +338,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription1 = createSubscription(criteria1, payload);
Subscription subscription2 = createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
Observation observation1 = sendObservation(code, "SNOMED-CT");
@ -318,7 +416,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription1 = createSubscription(criteria1, payload);
Subscription subscription2 = createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
ourLog.info("** About to send obervation");
Observation observation1 = sendObservation(code, "SNOMED-CT");
@ -384,7 +482,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
@Test
public void testSubscriptionTriggerViaSubscription() throws Exception {
BaseSubscriptionInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(true);
SubscriptionMatcherInterceptor.setForcePayloadEncodeAndDecodeForUnitTests(true);
String payload = "application/xml";
@ -392,7 +490,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
createSubscription(criteria1, payload);
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
ourLog.info("** About to send obervation");
@ -490,7 +588,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
Subscription subscription1 = createSubscription(criteria1, payload);
Subscription subscription2 = createSubscription(criteria2, payload);
waitForRegisteredSubscriptionCount(2);
waitForActivatedSubscriptionCount(2);
Observation observation1 = sendObservation(code, "SNOMED-CT");
@ -526,7 +624,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
// Add some headers, and we'll also turn back to requested status for fun
Subscription subscription = createSubscription(criteria1, payload);
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
subscription.getChannel().addHeader("X-Foo: FOO");
subscription.getChannel().addHeader("X-Bar: BAR");
@ -553,7 +651,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
Subscription subscription = createSubscription(criteria1, payload);
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
sendObservation(code, "SNOMED-CT");
@ -644,7 +742,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
mySearchParameterDao.create(sp);
mySearchParamRegsitry.forceRefresh();
createSubscription(criteria, "application/json");
waitForRegisteredSubscriptionCount(1);
waitForActivatedSubscriptionCount(1);
{
Observation bodySite = new Observation();

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Observation;
@ -25,6 +26,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.List;
@ -42,6 +44,9 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B
private static String ourListenerServerBase;
private static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void afterUnregisterRestHookListener() {
ourLog.info("** AFTER **");
@ -51,12 +56,12 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.getInterceptors().remove(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterRestHookListener() {
myDaoConfig.getInterceptors().add(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
@ -64,11 +69,11 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B
ourCreatedObservations.clear();
ourUpdatedObservations.clear();
ourRestHookSubscriptionInterceptor.initSubscriptions();
mySubscriptionLoader.initSubscriptions();
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}

View File

@ -1,29 +1,30 @@
package ca.uhn.fhir.jpa.subscription;
import static org.junit.Assert.*;
import java.util.Collections;
import java.util.List;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionActivatingInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import com.google.common.collect.Lists;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import java.util.Collections;
import java.util.List;
/**
* Test the rest-hook subscriptions
@ -38,6 +39,9 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test extends B
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.class);
private static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
protected boolean shouldLogClient() {
return false;
@ -51,20 +55,20 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test extends B
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.getInterceptors().remove(ourRestHookSubscriptionInterceptor);
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(false);
}
@Before
public void beforeRegisterRestHookListener() {
myDaoConfig.getInterceptors().add(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
public void beforeReset() {
ourCreatedObservations.clear();
ourUpdatedObservations.clear();
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
}
private Subscription createSubscription(String criteria, String payload, String endpoint) throws InterruptedException {
@ -87,7 +91,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test extends B
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}
private Observation sendObservation(String code, String system) throws InterruptedException {

View File

@ -1,28 +1,30 @@
package ca.uhn.fhir.jpa.subscription.r4;
import java.util.Collections;
import java.util.List;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.*;
import com.google.common.collect.Lists;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import com.google.common.collect.Lists;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.List;
/**
* Test the rest-hook subscriptions
@ -37,6 +39,9 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.class);
private static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
protected boolean shouldLogClient() {
return false;
@ -50,12 +55,12 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
myDaoConfig.getInterceptors().remove(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Before
public void beforeRegisterRestHookListener() {
myDaoConfig.getInterceptors().add(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before
@ -84,11 +89,11 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
}
private void waitForQueueToDrain() throws InterruptedException {
ourLog.info("QUEUE HAS {} ITEMS", getRestHookSubscriptionInterceptor().getExecutorQueueSizeForUnitTests());
while (getRestHookSubscriptionInterceptor().getExecutorQueueSizeForUnitTests() > 0) {
ourLog.info("QUEUE HAS {} ITEMS", mySubscriptionTestUtil.getExecutorQueueSizeForUnitTests());
while (mySubscriptionTestUtil.getExecutorQueueSizeForUnitTests() > 0) {
Thread.sleep(250);
}
ourLog.info("QUEUE HAS {} ITEMS", getRestHookSubscriptionInterceptor().getExecutorQueueSizeForUnitTests());
ourLog.info("QUEUE HAS {} ITEMS", mySubscriptionTestUtil.getExecutorQueueSizeForUnitTests());
}
private Observation sendObservation(String code, String system) throws InterruptedException {

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.FhirR4Util;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.rest.api.MethodOutcome;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IIdType;
@ -11,6 +13,7 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Collections;
@ -44,6 +47,9 @@ public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Tes
private String mySubscriptionId;
private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
@After
public void after() throws Exception {
@ -64,7 +70,7 @@ public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Tes
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@Override
@ -76,6 +82,9 @@ public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Tes
}
/**
* Ignored because this feature isn't implemented yet
*/
@Test
@Ignore
public void testSubscriptionAddedTrigger() {
@ -122,7 +131,7 @@ public class RestHookWithEventDefinitionR4Test extends BaseResourceProviderR4Tes
@Before
public void beforeRegisterRestHookListener() {
ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor());
mySubscriptionTestUtil.registerRestHookInterceptor();
}
@Before

View File

@ -1,9 +1,11 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.resthook;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.jpa.subscription.SubscriptionTriggeringSvcImpl;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
@ -50,6 +52,9 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
private static List<String> ourContentTypes = new ArrayList<>();
private List<IIdType> mySubscriptionIds = new ArrayList<>();
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void afterUnregisterRestHookListener() {
ourLog.info("**** Starting @After *****");
@ -66,7 +71,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
ourLog.info("Done deleting all subscriptions");
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
ourRestServer.unregisterInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
mySubscriptionTriggeringSvc.cancelAll();
mySubscriptionTriggeringSvc.setMaxSubmitPerPass(null);
@ -79,7 +84,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
@Before
public void beforeRegisterRestHookListener() {
ourRestServer.registerInterceptor(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.registerRestHookInterceptor();
}
/**
@ -376,7 +381,7 @@ public class SubscriptionTriggeringDstu3Test extends BaseResourceProviderDstu3Te
}
private void waitForQueueToDrain() throws InterruptedException {
RestHookTestDstu2Test.waitForQueueToDrain(ourRestHookSubscriptionInterceptor);
mySubscriptionTestUtil.waitForQueueToDrain();
}
public static class ObservationListener implements IResourceProvider {

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.FhirDstu2Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
@ -27,7 +29,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
// This is currently disabled as the criteria mechanism was a non-standard experiment
@Ignore

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.FhirDstu3Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.eclipse.jetty.websocket.api.Session;
@ -18,7 +20,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
// This is currently disabled as the criteria mechanism was a non-standard experiment
@Ignore

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.FhirR4Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -19,7 +20,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
// This is currently disabled as the criteria mechanism was a non-standard experiment

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderDstu2Test;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import ca.uhn.fhir.jpa.subscription.FhirDstu2Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt;
@ -21,13 +23,14 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
/**
* Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the
@ -52,11 +55,13 @@ public class WebsocketWithSubscriptionIdDstu2Test extends BaseResourceProviderDs
private WebSocketClient myWebSocketClient;
private SocketImplementation mySocketImplementation;
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void after() throws Exception {
super.after();
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.unregisterInterceptor(interceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@After
@ -69,8 +74,7 @@ public class WebsocketWithSubscriptionIdDstu2Test extends BaseResourceProviderDs
public void before() throws Exception {
super.before();
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.registerInterceptor(interceptor);
mySubscriptionTestUtil.registerWebSocketInterceptor();
/*
* Create patient

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import ca.uhn.fhir.jpa.subscription.FhirDstu3Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.eclipse.jetty.websocket.api.Session;
@ -12,13 +14,14 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
/**
* Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the
@ -46,12 +49,14 @@ public class WebsocketWithSubscriptionIdDstu3Test extends BaseResourceProviderDs
private WebSocketClient myWebSocketClient;
private SocketImplementation mySocketImplementation;
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@After
public void after() throws Exception {
super.after();
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.unregisterInterceptor(interceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@After
@ -67,8 +72,7 @@ public class WebsocketWithSubscriptionIdDstu3Test extends BaseResourceProviderDs
myDaoConfig.setSubscriptionEnabled(true);
myDaoConfig.setSubscriptionPollDelay(0L);
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.registerInterceptor(interceptor);
mySubscriptionTestUtil.registerWebSocketInterceptor();
/*
* Create patient

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.jpa.subscription.r4;
package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.subscription.FhirR4Util;
import ca.uhn.fhir.jpa.subscription.SocketImplementation;
import ca.uhn.fhir.jpa.subscription.websocket.SubscriptionWebsocketInterceptor;
import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.eclipse.jetty.websocket.api.Session;
@ -13,13 +14,14 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.net.URI;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
/**
* Adds a FHIR subscription with criteria through the rest interface. Then creates a websocket with the id of the
@ -47,12 +49,14 @@ public class WebsocketWithSubscriptionIdR4Test extends BaseResourceProviderR4Tes
private WebSocketClient myWebSocketClient;
private SocketImplementation mySocketImplementation;
@Autowired
private SubscriptionTestUtil mySubscriptionTestUtil;
@Override
@After
public void after() throws Exception {
super.after();
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.unregisterInterceptor(interceptor);
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
}
@After
@ -66,8 +70,7 @@ public class WebsocketWithSubscriptionIdR4Test extends BaseResourceProviderR4Tes
public void before() throws Exception {
super.before();
SubscriptionWebsocketInterceptor interceptor = ourWebApplicationContext.getBean(SubscriptionWebsocketInterceptor.class);
ourRestServer.registerInterceptor(interceptor);
mySubscriptionTestUtil.registerWebSocketInterceptor();
/*
* Create patient

View File

@ -1,4 +1,18 @@
## Running hapi-fhir-jpaserver-example in Tomcat from IntelliJ
# Unsupported
This [hapi-fhir-jpaserver-example](https://github.com/jamesagnew/hapi-fhir/tree/master/hapi-fhir-jpaserver-example) project is no longer supported.
## Supported JPA Example
The supported HAPI-FHIR JPA example is available in the [hapi-fhir-jpaserver-starter](https://github.com/hapifhir/hapi-fhir-jpaserver-starter)
project within the [hapifhir](https://github.com/hapifhir) GitHub Organization.
## Previous Documentation
Below is the original documentation for this project. Note that this documentation is no longer being updated.
#### Running hapi-fhir-jpaserver-example in Tomcat from IntelliJ
Install Tomcat.
@ -33,7 +47,7 @@ Point your browser (or fiddler, or what have you) to `http://localhost:8080/hapi
You should get an empty bundle back.
## Running hapi-fhir-jpaserver-example in a Docker container
#### Running hapi-fhir-jpaserver-example in a Docker container
Execute the `build-docker-image.sh` script to build the docker image.

View File

@ -1,23 +1,6 @@
package ca.uhn.fhir.jpa.demo;
import java.util.Collection;
import java.util.List;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.DaoConfig;
@ -26,12 +9,22 @@ import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.List;
public class JpaServerDemo extends RestfulServer {
@ -134,12 +127,10 @@ public class JpaServerDemo extends RestfulServer {
setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class));
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
* Register interceptors for the server based on DaoConfig.getSupportedSubscriptionTypes()
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
/*
* If you are hosting this server at a specific DNS name, the server will try to

View File

@ -1,16 +1,6 @@
package ca.uhn.fhir.jpa.demo;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.dao.DaoConfig;
@ -20,12 +10,20 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.jpa.subscription.SubscriptionInterceptorLoader;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import javax.servlet.ServletException;
import java.util.List;
public class JpaServerDemoDstu2 extends RestfulServer {
@ -128,12 +126,10 @@ public class JpaServerDemoDstu2 extends RestfulServer {
setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class));
/*
* Load interceptors for the server from Spring (these are defined in FhirServerConfig.java)
* Register interceptors for the server based on DaoConfig.getSupportedSubscriptionTypes()
*/
Collection<IServerInterceptor> interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values();
for (IServerInterceptor interceptor : interceptorBeans) {
this.registerInterceptor(interceptor);
}
SubscriptionInterceptorLoader subscriptionInterceptorLoader = myAppCtx.getBean(SubscriptionInterceptorLoader.class);
subscriptionInterceptorLoader.registerInterceptors();
/*
* If you are hosting this server at a specific DNS name, the server will try to

View File

@ -20,8 +20,10 @@ package ca.uhn.fhir.jpa.model.entity;
* #L%
*/
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.Subscription;
import java.util.Arrays;
import java.util.Collections;
@ -53,6 +55,8 @@ public class ModelConfig {
private Set<String> myTreatBaseUrlsAsLocal = new HashSet<>();
private Set<String> myTreatReferencesAsLogical = new HashSet<>(DEFAULT_LOGICAL_BASE_URLS);
private boolean myDefaultSearchParamsCanBeOverridden = false;
private Set<Subscription.SubscriptionChannelType> mySupportedSubscriptionTypes = new HashSet<>();
private String myEmailFromAddress = "noreply@unknown.com";
/**
* If set to {@code true} the default search params (i.e. the search parameters that are
@ -297,6 +301,46 @@ public class ModelConfig {
return this;
}
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public ModelConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) {
mySupportedSubscriptionTypes.add(theSubscriptionChannelType);
return this;
}
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public Set<Subscription.SubscriptionChannelType> getSupportedSubscriptionTypes() {
return Collections.unmodifiableSet(mySupportedSubscriptionTypes);
}
@VisibleForTesting
public void clearSupportedSubscriptionTypesForUnitTest() {
mySupportedSubscriptionTypes.clear();
}
/**
* If e-mail subscriptions are supported, the From address used when sending e-mails
*/
public String getEmailFromAddress() {
return myEmailFromAddress;
}
/**
* If e-mail subscriptions are supported, the From address used when sending e-mails
*/
public void setEmailFromAddress(String theEmailFromAddress) {
myEmailFromAddress = theEmailFromAddress;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty");
@ -309,5 +353,4 @@ public class ModelConfig {
}
}

View File

@ -38,9 +38,6 @@ import org.hl7.fhir.r4.model.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -61,9 +58,6 @@ public class ResourceLinkExtractor {
@Autowired
private ISearchParamExtractor mySearchParamExtractor;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
protected EntityManager myEntityManager;
public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver) {
String resourceType = theEntity.getResourceType();

View File

@ -36,7 +36,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.PostConstruct;
@ -45,29 +44,20 @@ import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implements ISearchParamRegistry {
@Autowired
private ModelConfig myModelConfig;
@Autowired
private ISearchParamProvider mySearchParamProvider;
@Autowired
private FhirContext myFhirContext;
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class);
private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams;
private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
@Autowired
private FhirContext myCtx;
private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
@Autowired
private ModelConfig myModelConfig;
private volatile long myLastRefresh;
private ISearchParamProvider mySearchParamProvider;
BaseSearchParamRegistry(ISearchParamProvider theSearchParamProvider) {
super();
mySearchParamProvider = theSearchParamProvider;
}
@VisibleForTesting
public void setSearchParamProvider(ISearchParamProvider theSearchParamProvider) {
mySearchParamProvider = theSearchParamProvider;
}
@Override
public void requestRefresh() {
@ -219,10 +209,10 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
public void postConstruct() {
Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
Set<String> resourceNames = myCtx.getResourceNames();
Set<String> resourceNames = myFhirContext.getResourceNames();
for (String resourceName : resourceNames) {
RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(resourceName);
RuntimeResourceDefinition nextResDef = myFhirContext.getResourceDefinition(resourceName);
String nextResourceName = nextResDef.getName();
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
resourceNameToSearchParams.put(nextResourceName, nameToParam);
@ -283,7 +273,7 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
continue;
}
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myCtx, nextSp)) {
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, nextSp)) {
if (isBlank(nextBaseName)) {
continue;
}
@ -348,5 +338,9 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen
return getActiveSearchParams(theResourceDef.getName()).values();
}
@VisibleForTesting
@Override
public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) {
mySearchParamProvider = theSearchParamProvider;
}
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import com.google.common.annotations.VisibleForTesting;
import java.util.Collection;
import java.util.List;
@ -59,4 +60,7 @@ public interface ISearchParamRegistry {
RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName);
Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef);
@VisibleForTesting
void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider);
}

View File

@ -39,10 +39,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry<SearchParameter> {
public SearchParamRegistryDstu2(ISearchParamProvider theSearchParamProvider) {
super(theSearchParamProvider);
}
@Override
protected JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) {
String name = theNextSp.getCode();

View File

@ -39,10 +39,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry<SearchParameter> {
public SearchParamRegistryDstu3(ISearchParamProvider theSearchParamProvider) {
super(theSearchParamProvider);
}
@Override
protected JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) {
String name = theNextSp.getCode();

View File

@ -41,10 +41,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchParamRegistryR4 extends BaseSearchParamRegistry<SearchParameter> {
public SearchParamRegistryR4(ISearchParamProvider theSearchParamProvider) {
super(theSearchParamProvider);
}
@Override
protected RuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) {
String name = theNextSp.getCode();
@ -128,6 +124,4 @@ public class SearchParamRegistryR4 extends BaseSearchParamRegistry<SearchParamet
return new JpaRuntimeSearchParam(id, uri, name, description, path, paramType, providesMembershipInCompartments, targets, status, unique, components, theNextSp.getBase());
}
}

View File

@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
@ -86,6 +87,11 @@ public class SearchParamExtractorDstu3Test {
public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) {
return null;
}
@Override
public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) {
// nothing
}
};
SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry);

View File

@ -29,6 +29,47 @@
<artifactId>hapi-fhir-validation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<exclusions>
<exclusion>
<artifactId>xml-apis</artifactId>
<groupId>xml-apis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- test dependencies -->
<dependency>
@ -46,6 +87,16 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.module;
/*-
* #%L
@ -54,7 +54,7 @@ public class CanonicalSubscription implements Serializable {
@JsonProperty("headers")
private List<String> myHeaders;
@JsonProperty("channelType")
private Subscription.SubscriptionChannelType myChannelType;
private CanonicalSubscriptionChannelType myChannelType;
@JsonProperty("status")
private Subscription.SubscriptionStatus myStatus;
@JsonProperty("triggerDefinition")
@ -73,11 +73,11 @@ public class CanonicalSubscription implements Serializable {
}
public Subscription.SubscriptionChannelType getChannelType() {
public CanonicalSubscriptionChannelType getChannelType() {
return myChannelType;
}
public void setChannelType(Subscription.SubscriptionChannelType theChannelType) {
public void setChannelType(CanonicalSubscriptionChannelType theChannelType) {
myChannelType = theChannelType;
}

View File

@ -0,0 +1,115 @@
package ca.uhn.fhir.jpa.subscription.module;
import org.hl7.fhir.exceptions.FHIRException;
public enum CanonicalSubscriptionChannelType {
/**
* The channel is executed by making a post to the URI. If a payload is included, the URL is interpreted as the service base, and an update (PUT) is made.
*/
RESTHOOK,
/**
* The channel is executed by sending a packet across a web socket connection maintained by the client. The URL identifies the websocket, and the client binds to this URL.
*/
WEBSOCKET,
/**
* The channel is executed by sending an email to the email addressed in the URI (which must be a mailto:).
*/
EMAIL,
/**
* The channel is executed by sending an SMS message to the phone number identified in the URL (tel:).
*/
SMS,
/**
* The channel is executed by sending a message (e.g. a Bundle with a MessageHeader resource etc.) to the application identified in the URI.
*/
MESSAGE,
/**
* added to help the parsers with the generic types
*/
NULL;
public static CanonicalSubscriptionChannelType fromCode(String codeString) throws FHIRException {
if (codeString == null || "".equals(codeString))
return null;
if ("rest-hook".equals(codeString))
return RESTHOOK;
if ("websocket".equals(codeString))
return WEBSOCKET;
if ("email".equals(codeString))
return EMAIL;
if ("sms".equals(codeString))
return SMS;
if ("message".equals(codeString))
return MESSAGE;
else
throw new FHIRException("Unknown SubscriptionChannelType code '" + codeString + "'");
}
public String toCode() {
switch (this) {
case RESTHOOK:
return "rest-hook";
case WEBSOCKET:
return "websocket";
case EMAIL:
return "email";
case SMS:
return "sms";
case MESSAGE:
return "message";
default:
return "?";
}
}
public String getSystem() {
switch (this) {
case RESTHOOK:
return "http://hl7.org/fhir/subscription-channel-type";
case WEBSOCKET:
return "http://hl7.org/fhir/subscription-channel-type";
case EMAIL:
return "http://hl7.org/fhir/subscription-channel-type";
case SMS:
return "http://hl7.org/fhir/subscription-channel-type";
case MESSAGE:
return "http://hl7.org/fhir/subscription-channel-type";
default:
return "?";
}
}
public String getDefinition() {
switch (this) {
case RESTHOOK:
return "The channel is executed by making a post to the URI. If a payload is included, the URL is interpreted as the service base, and an update (PUT) is made.";
case WEBSOCKET:
return "The channel is executed by sending a packet across a web socket connection maintained by the client. The URL identifies the websocket, and the client binds to this URL.";
case EMAIL:
return "The channel is executed by sending an email to the email addressed in the URI (which must be a mailto:).";
case SMS:
return "The channel is executed by sending an SMS message to the phone number identified in the URL (tel:).";
case MESSAGE:
return "The channel is executed by sending a message (e.g. a Bundle with a MessageHeader resource etc.) to the application identified in the URI.";
default:
return "?";
}
}
public String getDisplay() {
switch (this) {
case RESTHOOK:
return "Rest Hook";
case WEBSOCKET:
return "Websocket";
case EMAIL:
return "Email";
case SMS:
return "SMS";
case MESSAGE:
return "Message";
default:
return "?";
}
}
}

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.jpa.subscription.module;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import java.util.concurrent.LinkedBlockingQueue;
public class LinkedBlockingQueueSubscriptionChannel extends SubscriptionChannel {
public LinkedBlockingQueueSubscriptionChannel(String theThreadNamingPattern) {
super(new LinkedBlockingQueue<>(SubscriptionConstants.DELIVERY_EXECUTOR_QUEUE_SIZE), theThreadNamingPattern);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.subscription;
package ca.uhn.fhir.jpa.subscription.module;
/*-
* #%L
@ -53,6 +53,18 @@ public class ResourceModifiedMessage {
@JsonIgnore
private transient IBaseResource myPayloadDecoded;
// For JSON
public ResourceModifiedMessage() {
}
public ResourceModifiedMessage(FhirContext theFhirContext, IBaseResource theResource, OperationTypeEnum theOperationType) {
setId(theResource.getIdElement());
setOperationType(theOperationType);
if (theOperationType != OperationTypeEnum.DELETE) {
setNewPayload(theFhirContext, theResource);
}
}
public String getPayloadId() {
return myPayloadId;
}

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.jpa.subscription.module;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
import java.util.ArrayList;
import java.util.concurrent.*;
public class SubscriptionChannel implements SubscribableChannel {
private Logger ourLog = LoggerFactory.getLogger(SubscriptionChannel.class);
private final ExecutorSubscribableChannel mySubscribableChannel;
private final BlockingQueue<Runnable> myQueue;
public SubscriptionChannel(BlockingQueue<Runnable> theQueue, String theThreadNamingPattern) {
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern(theThreadNamingPattern)
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = (theRunnable, theExecutor) -> {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", theQueue.size());
StopWatch sw = new StopWatch();
try {
theQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
SubscriptionConstants.EXECUTOR_THREAD_COUNT,
0L,
TimeUnit.MILLISECONDS,
theQueue,
threadFactory,
rejectedExecutionHandler);
myQueue = theQueue;
mySubscribableChannel = new ExecutorSubscribableChannel(executor);
}
@Override
public boolean subscribe(MessageHandler handler) {
return mySubscribableChannel.subscribe(handler);
}
@Override
public boolean unsubscribe(MessageHandler handler) {
return mySubscribableChannel.unsubscribe(handler);
}
@Override
public boolean send(Message<?> message, long timeout) {
return mySubscribableChannel.send(message, timeout);
}
@VisibleForTesting
public void clearInterceptorsForUnitTest() {
mySubscribableChannel.setInterceptors(new ArrayList<>());
}
@VisibleForTesting
public void addInterceptorForUnitTest(ChannelInterceptor theInterceptor) {
mySubscribableChannel.addInterceptor(theInterceptor);
}
@VisibleForTesting
public int getQueueSizeForUnitTest() {
return myQueue.size();
}
}

View File

@ -0,0 +1,73 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import java.util.Collection;
import java.util.HashSet;
public class ActiveSubscription {
private static final Logger ourLog = LoggerFactory.getLogger(ActiveSubscription.class);
private final CanonicalSubscription mySubscription;
private final SubscribableChannel mySubscribableChannel;
private final Collection<MessageHandler> myDeliveryHandlerSet = new HashSet<>();
public ActiveSubscription(CanonicalSubscription theSubscription, SubscribableChannel theSubscribableChannel) {
mySubscription = theSubscription;
mySubscribableChannel = theSubscribableChannel;
}
public CanonicalSubscription getSubscription() {
return mySubscription;
}
public SubscribableChannel getSubscribableChannel() {
return mySubscribableChannel;
}
public void register(MessageHandler theHandler) {
mySubscribableChannel.subscribe(theHandler);
myDeliveryHandlerSet.add(theHandler);
}
public void unregister(MessageHandler theMessageHandler) {
if (mySubscribableChannel != null) {
mySubscribableChannel.unsubscribe(theMessageHandler);
if (mySubscribableChannel instanceof DisposableBean) {
try {
((DisposableBean) mySubscribableChannel).destroy();
} catch (Exception e) {
ourLog.error("Failed to destroy channel bean", e);
}
}
}
}
public void unregisterAll() {
for (MessageHandler messageHandler : myDeliveryHandlerSet) {
unregister(messageHandler);
}
}
public IIdType getIdElement(FhirContext theFhirContext) {
return mySubscription.getIdElement(theFhirContext);
}
public String getCriteriaString() {
return mySubscription.getCriteriaString();
}
@VisibleForTesting
public MessageHandler getDeliveryHandlerForUnitTest() {
return myDeliveryHandlerSet.iterator().next();
}
}

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ActiveSubscriptionCache {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ActiveSubscriptionCache.class);
private final Map<String, ActiveSubscription> myCache = new ConcurrentHashMap<>();
public ActiveSubscription get(String theIdPart) {
return myCache.get(theIdPart);
}
public Collection<ActiveSubscription> getAll() {
return Collections.unmodifiableCollection(myCache.values());
}
public int size() {
return myCache.size();
}
public void put(String theSubscriptionId, ActiveSubscription theValue) {
myCache.put(theSubscriptionId, theValue);
}
public void remove(String theSubscriptionId) {
Validate.notBlank(theSubscriptionId);
ActiveSubscription activeSubscription = myCache.get(theSubscriptionId);
if (activeSubscription == null) {
return;
}
activeSubscription.unregisterAll();
myCache.remove(theSubscriptionId);
}
public void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) {
for (String next : new ArrayList<>(myCache.keySet())) {
if (!theAllIds.contains(next)) {
ourLog.info("Unregistering Subscription/{}", next);
remove(next);
}
}
}
}

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.jpa.subscription.module.SubscriptionChannel;
import ca.uhn.fhir.jpa.subscription.module.LinkedBlockingQueueSubscriptionChannel;
public class BlockingQueueSubscriptionChannelFactory implements ISubscriptionChannelFactory {
@Override
public SubscriptionChannel newDeliveryChannel(String theSubscriptionId, String theChannelType) {
String threadName = "subscription-delivery-" +
theChannelType +
"-" +
theSubscriptionId +
"-%d";
return new LinkedBlockingQueueSubscriptionChannel(threadName);
}
@Override
public SubscriptionChannel newMatchingChannel(String theChannelName) {
return new LinkedBlockingQueueSubscriptionChannel(theChannelName + "-%d");
}
}

View File

@ -0,0 +1,9 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import org.springframework.messaging.SubscribableChannel;
public interface ISubscriptionChannelFactory {
SubscribableChannel newDeliveryChannel(String theSubscriptionId, String theChannelType);
SubscribableChannel newMatchingChannel(String theChannelName);
}

View File

@ -0,0 +1,11 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface ISubscriptionProvider {
IBundleProvider search(SearchParameterMap theMap);
boolean loadSubscription(IBaseResource theResource);
}

View File

@ -0,0 +1,149 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SubscriptionCannonicalizer<S extends IBaseResource> {
@Autowired
FhirContext myFhirContext;
public CanonicalSubscription canonicalize(S theSubscription) {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU2:
return canonicalizeDstu2(theSubscription);
case DSTU3:
return canonicalizeDstu3(theSubscription);
case R4:
return canonicalizeR4(theSubscription);
default:
throw new ConfigurationException("Subscription not supported for version: " + myFhirContext.getVersion().getVersion());
}
}
protected CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
try {
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
retVal.setChannelType(CanonicalSubscriptionChannelType.fromCode(subscription.getChannel().getType()));
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
} catch (FHIRException theE) {
throw new InternalErrorException(theE);
}
return retVal;
}
protected CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
try {
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus().toCode()));
retVal.setChannelType(CanonicalSubscriptionChannelType.fromCode(subscription.getChannel().getType().toCode()));
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
String from;
String subjectTemplate;
String bodyTemplate;
try {
from = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
}
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
String stripVersionIds;
String deliverLatestVersion;
try {
stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
}
} catch (FHIRException theE) {
throw new InternalErrorException(theE);
}
return retVal;
}
protected CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
retVal.setStatus(subscription.getStatus());
retVal.setChannelType(CanonicalSubscriptionChannelType.fromCode(subscription.getChannel().getType().toCode()));
retVal.setCriteriaString(subscription.getCriteria());
retVal.setEndpointUrl(subscription.getChannel().getEndpoint());
retVal.setHeaders(subscription.getChannel().getHeader());
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
String from;
String subjectTemplate;
try {
from = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM);
subjectTemplate = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getEmailDetails().setFrom(from);
retVal.getEmailDetails().setSubjectTemplate(subjectTemplate);
}
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
String stripVersionIds;
String deliverLatestVersion;
try {
stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS);
deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION);
} catch (FHIRException theE) {
throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE);
}
retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds));
retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion));
}
List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics");
if (topicExts.size() > 0) {
IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive();
if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) {
throw new PreconditionFailedException("Topic reference must be an EventDefinition");
}
}
return retVal;
}
}

View File

@ -0,0 +1,70 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
public class SubscriptionConstants {
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from";
/**
* <p>
* This extension should be of type <code>string</code> and should be
* placed on the <code>Subscription.channel</code> element
* </p>
*/
public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template";
/**
* This extension URL indicates whether a REST HOOK delivery should
* include the version ID when delivering.
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
public static final String EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids";
/**
* This extension URL indicates whether a REST HOOK delivery should
* reload the resource and deliver the latest version always. This
* could be useful for example if a resource which triggers a
* subscription gets updated many times in short succession and there
* is no value in delivering the older versions.
* <p>
* Note that if the resource is now deleted, this may cause
* the delivery to be cancelled altogether.
* </p>
*
* <p>
* This extension should be of type <code>boolean</code> and should be
* placed on the <code>Subscription.channel</code> element.
* </p>
*/
public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version";
/**
* The number of threads used in subscription channel processing
*/
public static final int EXECUTOR_THREAD_COUNT = 5;
/**
* The maximum number of subscriptions that can be active at once
*/
public static final int MAX_SUBSCRIPTION_RESULTS = 1000;
/**
* The size of the queue used for sending resources to the subscription matching processor
*/
public static final int PROCESSING_EXECUTOR_QUEUE_SIZE = 1000;
/**
* The size of the queue used by each subscription delivery queue
*/
public static final int DELIVERY_EXECUTOR_QUEUE_SIZE = 1000;
}

View File

@ -0,0 +1,37 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType;
import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionDeliveringRestHookSubscriber;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.IEmailSender;
import ca.uhn.fhir.jpa.subscription.module.subscriber.email.SubscriptionDeliveringEmailSubscriber;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public abstract class SubscriptionDeliveryHandlerFactory {
private IEmailSender myEmailSender;
@Lookup
protected abstract SubscriptionDeliveringEmailSubscriber getSubscriptionDeliveringEmailSubscriber(IEmailSender myEmailSender);
@Lookup
protected abstract SubscriptionDeliveringRestHookSubscriber getSubscriptionDeliveringRestHookSubscriber();
public Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
if (theSubscription.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
return Optional.of(getSubscriptionDeliveringEmailSubscriber(myEmailSender));
} else if (theSubscription.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
return Optional.of(getSubscriptionDeliveringRestHookSubscriber());
} else {
return Optional.empty();
}
}
public void setEmailSender(IEmailSender theEmailSender) {
myEmailSender = theEmailSender;
}
}

View File

@ -0,0 +1,103 @@
package ca.uhn.fhir.jpa.subscription.module.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
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.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
@Service
@Lazy
public class SubscriptionLoader {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class);
@Autowired
private ISubscriptionProvider mySubscriptionProvidor;
@Autowired
private SubscriptionRegistry mySubscriptionRegistry;
private final Object myInitSubscriptionsLock = new Object();
private Semaphore myInitSubscriptionsSemaphore = new Semaphore(1);
@PostConstruct
public void start() {
initSubscriptions();
}
/**
* Read the existing subscriptions from the database
*/
@SuppressWarnings("unused")
@Scheduled(fixedDelay = 60000)
public void initSubscriptions() {
if (!myInitSubscriptionsSemaphore.tryAcquire()) {
return;
}
try {
doInitSubscriptions();
} finally {
myInitSubscriptionsSemaphore.release();
}
}
@VisibleForTesting
public int doInitSubscriptionsForUnitTest() {
return doInitSubscriptions();
}
private int doInitSubscriptions() {
synchronized (myInitSubscriptionsLock) {
ourLog.debug("Starting init subscriptions");
SearchParameterMap map = new SearchParameterMap();
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
IBundleProvider subscriptionBundleList = mySubscriptionProvidor.search(map);
if (subscriptionBundleList.size() >= SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS) {
ourLog.error("Currently over " + SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS + " subscriptions. Some subscriptions have not been loaded.");
}
List<IBaseResource> resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size());
Set<String> allIds = new HashSet<>();
int changesCount = 0;
for (IBaseResource resource : resourceList) {
String nextId = resource.getIdElement().getIdPart();
allIds.add(nextId);
boolean changed = mySubscriptionProvidor.loadSubscription(resource);
if (changed) {
changesCount++;
}
}
mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
ourLog.trace("Finished init subscriptions - found {}", resourceList.size());
return changesCount;
}
}
@VisibleForTesting
public void setSubscriptionProviderForUnitTest(ISubscriptionProvider theSubscriptionProvider) {
mySubscriptionProvidor = theSubscriptionProvider;
}
}

Some files were not shown because too many files have changed in this diff Show More