diff --git a/.travis.yml b/.travis.yml index 911244c5d2f..eb54f367cd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ dist: trusty language: java jdk: - - oraclejdk11 + - openjdk11 env: global: - MAVEN_OPTS="-Xmx10244M -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1024M -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" diff --git a/example-projects/README.md b/example-projects/README.md new file mode 100644 index 00000000000..5ef27babc43 --- /dev/null +++ b/example-projects/README.md @@ -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. diff --git a/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java b/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java index 478accecb5b..c1fc5856894 100644 --- a/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java +++ b/example-projects/hapi-fhir-jpaserver-cds-example/src/test/java/ca/uhn/fhir/jpa/cds/example/CdsExampleTests.java @@ -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; diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 9dfa49e7a21..b6c86dc655b 100644 --- a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -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 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 diff --git a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java index cf6bb8572ff..3c802e6cb10 100644 --- a/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java +++ b/example-projects/hapi-fhir-jpaserver-dynamic/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java @@ -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 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 diff --git a/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index f45045cf755..93ae03951b4 100644 --- a/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/example-projects/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -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 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 diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 054dde91604..84ec5a98714 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -99,6 +99,10 @@ javax.mail javax.mail-api + + javax.activation + javax.activation-api + com.helger ph-schematron diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index bd368198eff..04014775ce1 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -48,6 +48,12 @@ com.helger ph-schematron true + + + org.glassfish.jaxb + jaxb-core + + com.helger diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java index b370ea014e1..8f2b8158781 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fluentpath/IFluentPath.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.fluentpath; */ import java.util.List; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBase; @@ -36,6 +37,15 @@ public interface IFluentPath { */ List evaluate(IBase theInput, String thePath, Class theReturnType); - + /** + * Apply the given FluentPath expression against the given input and return + * the first match (if any) + * + * @param theInput The input object (generally a resource or datatype) + * @param thePath The fluent path expression + * @param theReturnType The type to return (in order to avoid casting) + */ + Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index ec8f590d723..1e2330f5424 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -53,8 +53,8 @@ public class FhirTerser { if (theChildDefinition == null) return null; if (theCurrentList == null || theCurrentList.isEmpty()) - return new ArrayList(Arrays.asList(theChildDefinition.getElementName())); - List newList = new ArrayList(theCurrentList); + return new ArrayList<>(Arrays.asList(theChildDefinition.getElementName())); + List newList = new ArrayList<>(theCurrentList); newList.add(theChildDefinition.getElementName()); return newList; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 1ffeda550f2..92f20af6b75 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -218,13 +218,17 @@ javax.xml.bind jaxb-api + + + + + + + + - com.sun.xml.bind - jaxb-core - - - com.sun.xml.bind - jaxb-impl + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java index 66fac50c071..a77041fd8d3 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java @@ -256,27 +256,30 @@ public abstract class BaseApp { System.err.println("" + ansi().fg(Ansi.Color.WHITE).boldOff()); logCommandUsageNoHeader(command); runCleanupHookAndUnregister(); - System.exit(1); + exitDueToException(e); } catch (CommandFailureException e) { ourLog.error(e.getMessage()); runCleanupHookAndUnregister(); - if ("true".equals(System.getProperty("test"))) { - throw e; - } else { - System.exit(1); - } + exitDueToException(e); } catch (Throwable t) { ourLog.error("Error during execution: ", t); runCleanupHookAndUnregister(); - if ("true".equals(System.getProperty("test"))) { - throw new CommandFailureException("Error: " + t.toString(), t); - } else { - System.exit(1); - } + exitDueToException(new CommandFailureException("Error: " + t.toString(), t)); } } + private void exitDueToException(Throwable e) { + if ("true".equals(System.getProperty("test"))) { + if (e instanceof CommandFailureException) { + throw (CommandFailureException)e; + } + throw new Error(e); + } else { + System.exit(1); + } + } + private void runCleanupHookAndUnregister() { if (myShutdownHookHasNotRun) { Runtime.getRuntime().removeShutdownHook(myShutdownHook); diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index b4972e3e99d..c2bff0b1cfd 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -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 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(); } } diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 6f662773851..d0662b73e4a 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -231,14 +231,14 @@ javax.xml.bind jaxb-api - + @@ -360,18 +360,10 @@ xml-apis xml-apis - - org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec - - - javax.activation - activation - - - javax.activation - javax.activation-api - + + + + @@ -409,9 +401,14 @@ - javax.transaction - javax.transaction-api + com.sun.activation + javax.activation + 1.2.0 + + + + javax.mail javax.mail-api @@ -428,10 +425,10 @@ - - com.sun.activation - javax.activation - + + + + - javax.xml.bind - jaxb-api - ${jaxb_api_version} - - - com.sun.xml.bind - jaxb-core - ${jaxb_core_version} - - - com.sun.xml.bind - jaxb-impl - ${jaxb_core_version} + org.glassfish.jaxb + jaxb-runtime + ${jaxb_runtime_version} + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 18e151eb601..40141da5113 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -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"); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java index f0ca4a9eb7d..88c71f6b89e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/WebsocketDispatcherConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/WebsocketDispatcherConfig.java index 383b52ea84a..c7b31cdc26f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/WebsocketDispatcherConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/WebsocketDispatcherConfig.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index 22eb20d0a8b..38060e32461 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index fdc4e637a78..5224694a797 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index dc3213931af..cc514088696 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -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; @@ -174,23 +171,17 @@ public abstract class BaseHapiFhirDao implements IDao, @Autowired private ISearchDao mySearchDao; @Autowired - private ISearchParamExtractor mySearchParamExtractor; - @Autowired private ISearchParamPresenceSvc mySearchParamPresenceSvc; //@Autowired //private ISearchResultDao mySearchResultDao; @Autowired - private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao; - @Autowired - private BeanFactory beanFactory; - @Autowired private DaoRegistry myDaoRegistry; @Autowired - private SearchParamExtractorService mySearchParamExtractorService; - @Autowired private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor; @Autowired - private DatabaseSearchParamSynchronizer myDatabaseSearchParamSynchronizer; + private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; + @Autowired + private SearchBuilderFactory mySearchBuilderFactory; private ApplicationContext myApplicationContext; @@ -752,9 +743,9 @@ public abstract class BaseHapiFhirDao 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) { @@ -1412,7 +1403,7 @@ public abstract class BaseHapiFhirDao implements IDao, * Indexing */ if (thePerformIndexing) { - myDatabaseSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams); + myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, theEntity, existingParams); mySearchParamWithInlineReferencesExtractor.storeCompositeStringUniques(newParams, theEntity, existingParams); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 56476f422fd..a8ef25c7bee 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -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 extends BaseHapiFhirDao implements IFhirResourceDao { 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 myResourceType; private String mySecondaryPrimaryKeyParamName; - @Autowired - private MatchResourceUrlService myMatchResourceUrlService; @Override public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 022c4e3419d..76c7a03bab7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -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 myInterceptors; + private List 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 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 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, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java index 0c57157a14e..9ae8f6807e7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DatabaseSearchParamProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java similarity index 50% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java index 9eb56024454..c75d1b32dd1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.websocket; +package ca.uhn.fhir.jpa.dao; /*- * #%L @@ -20,24 +20,11 @@ package ca.uhn.fhir.jpa.subscription.websocket; * #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 createDeliveryHandler(CanonicalSubscription theSubscription) { - return Optional.empty(); - } - - @Override - public Subscription.SubscriptionChannelType getChannelType() { - return Subscription.SubscriptionChannelType.WEBSOCKET; - } - +import org.springframework.beans.factory.annotation.Lookup; +import org.springframework.stereotype.Service; +@Service +public abstract class SearchBuilderFactory { + @Lookup + public abstract SearchBuilder newSearchBuilder(BaseHapiFhirDao theBaseHapiFhirResourceDao); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java index fe920db14bc..c55d7430acb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoResourceLinkResolver.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java index 27fe5d219f7..9e7a3ad1d66 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DatabaseSearchParamSynchronizer.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java @@ -35,7 +35,7 @@ import java.util.Collection; import java.util.List; @Service -public class DatabaseSearchParamSynchronizer { +public class DaoSearchParamSynchronizer { @Autowired private DaoConfig myDaoConfig; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java index b8ba6b4efb6..631eab984e8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -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) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java index 12ae5e73bab..95f862f081d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java @@ -28,7 +28,9 @@ import ca.uhn.fhir.rest.server.IPagingProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -@Service +// Note: this class is not annotated with @Service because we want to +// explicitly define it in BaseConfig.java. This is done so that +// implementors can override if they want to. public class DatabaseBackedPagingProvider extends BasePagingProvider implements IPagingProvider { @Autowired diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java index 7963413b83c..56a4d5d0876 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/IResourceReindexingSvc.java @@ -58,4 +58,6 @@ public interface IResourceReindexingSvc { * to be used by unit tests. */ void cancelAndPurgeAllJobs(); + + int countReindexJobs(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java index 362fa8675e3..f9048b66d7a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java @@ -30,11 +30,11 @@ import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; -import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; +import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.util.StopWatch; import com.google.common.annotations.VisibleForTesting; @@ -98,6 +98,8 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { private FhirContext myContext; @PersistenceContext(type = PersistenceContextType.TRANSACTION) private EntityManager myEntityManager; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; @VisibleForTesting void setReindexJobDaoForUnitTest(IResourceReindexJobDao theReindexJobDao) { @@ -186,7 +188,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { runReindexingPass(); } - @Override @Transactional(Transactional.TxType.NEVER) public Integer runReindexingPass() { @@ -203,7 +204,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { return null; } - private Integer doReindexingPassInsideLock() { + private int doReindexingPassInsideLock() { expungeJobsMarkedAsDeleted(); return runReindexJobs(); } @@ -233,13 +234,13 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { } private int runReindexJobs() { - Collection jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false)); - assert jobs != null; + Collection jobs = getResourceReindexJobEntities(); if (jobs.size() > 0) { ourLog.info("Running {} reindex jobs: {}", jobs.size(), jobs); } else { ourLog.debug("Running {} reindex jobs: {}", jobs.size(), jobs); + return 0; } int count = 0; @@ -255,6 +256,17 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { return count; } + @Override + public int countReindexJobs() { + return getResourceReindexJobEntities().size(); + } + + private Collection getResourceReindexJobEntities() { + Collection jobs = myTxTemplate.execute(t -> myReindexJobDao.findAll(PageRequest.of(0, 10), false)); + assert jobs != null; + return jobs; + } + private void markJobAsDeleted(ResourceReindexJobEntity theJob) { ourLog.info("Marking reindexing job ID[{}] as deleted", theJob.getId()); myTxTemplate.execute(t -> { @@ -263,6 +275,11 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { }); } + @VisibleForTesting + public void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) { + mySearchParamRegistry = theSearchParamRegistry; + } + private int runReindexJob(ResourceReindexJobEntity theJob) { if (theJob.getSuspendedUntil() != null) { if (theJob.getSuspendedUntil().getTime() > System.currentTimeMillis()) { @@ -274,6 +291,16 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { StopWatch sw = new StopWatch(); AtomicInteger counter = new AtomicInteger(); + /* + * On the first time we run a particular reindex job, let's make sure we + * have the latest search parameters loaded. A common reason to + * be reindexing is that the search parameters have changed in some way, so + * this makes sure we're on the latest versions + */ + if (theJob.getThresholdLow() == null) { + mySearchParamRegistry.forceRefresh(); + } + // Calculate range Date low = theJob.getThresholdLow() != null ? theJob.getThresholdLow() : BEGINNING_OF_TIME; Date high = theJob.getThresholdHigh(); @@ -461,7 +488,7 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceTable.getResourceType()); long expectedVersion = resourceTable.getVersion(); - IBaseResource resource = dao.read(resourceTable.getIdDt().toVersionless(), null,true); + IBaseResource resource = dao.read(resourceTable.getIdDt().toVersionless(), null, true); if (resource == null) { throw new InternalErrorException("Could not find resource version " + resourceTable.getIdDt().toUnqualified().getValue() + " in database"); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java deleted file mode 100644 index e1418316073..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java +++ /dev/null @@ -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 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 myDeliveryChannel; - private ExecutorService myProcessingExecutor; - private int myExecutorThreadCount; - private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber; - private MessageHandler mySubscriptionCheckingSubscriber; - private ConcurrentHashMap myIdToSubscription = new ConcurrentHashMap<>(); - private ConcurrentHashMap mySubscribableChannel = new ConcurrentHashMap<>(); - private Multimap myIdToDeliveryHandler = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class); - private ThreadPoolExecutor myDeliveryExecutor; - private LinkedBlockingQueue myProcessingExecutorQueue; - @Autowired - private FhirContext myCtx; - @Autowired(required = false) - @Qualifier("myEventDefinitionDaoR4") - private IFhirResourceDao 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 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 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 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 getIdToSubscription() { - return Collections.unmodifiableMap(myIdToSubscription); - } - - public SubscribableChannel getProcessingChannel() { - return myProcessingChannel; - } - - public void setProcessingChannel(SubscribableChannel theProcessingChannel) { - myProcessingChannel = theProcessingChannel; - } - - public List 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 resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); - - Set 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 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 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 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; - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java deleted file mode 100644 index 3a7d3fff7fc..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionSubscriber.java +++ /dev/null @@ -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; - } - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/DaoResourceRetriever.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/DaoResourceRetriever.java new file mode 100644 index 00000000000..3294d055600 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/DaoResourceRetriever.java @@ -0,0 +1,52 @@ +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.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()); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java new file mode 100644 index 00000000000..6563d8bbd31 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java @@ -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; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java deleted file mode 100644 index 44f488b9c54..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java +++ /dev/null @@ -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 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; - } - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java new file mode 100644 index 00000000000..50b9750428b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java @@ -0,0 +1,54 @@ +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.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 supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes(); + + if (!supportedSubscriptionTypes.isEmpty()) { + myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor); + myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); + } + } + + @VisibleForTesting + public void unregisterInterceptorsForUnitTest() { + myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor); + myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java new file mode 100644 index 00000000000..f07fd0b3ad0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java @@ -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; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java index 96bd3f9c032..01893c8a7e0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -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 myActiveJobs = new ArrayList<>(); + @Autowired private FhirContext myFhirContext; @Autowired private DaoRegistry myDaoRegistry; - private List> 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 myActiveJobs = new ArrayList<>(); + private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT; private ApplicationContext myAppCtx; private ExecutorService myExecutorService; @Override public IBaseParameters triggerSubscription(List theResourceIds, List 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> values = (Collection>) values1; - mySubscriptionInterceptorList.addAll(values); - - LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() .namingPattern("SubscriptionTriggering-%d") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java new file mode 100644 index 00000000000..7c63a0c1e2a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java @@ -0,0 +1,55 @@ +package ca.uhn.fhir.jpa.subscription.dbcache; + +/*- + * #%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.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); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java similarity index 52% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java index 788aa9ef139..84549dd1dbd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherCompositeInMemoryDatabase.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java @@ -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; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java similarity index 87% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java index 4614fde8207..6b1fbad53f0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherDatabase.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java deleted file mode 100644 index 0e3c85db169..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionEmailInterceptor.java +++ /dev/null @@ -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 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; - } - - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD deleted file mode 100644 index 22e4943bdad..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java~HEAD +++ /dev/null @@ -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); -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java deleted file mode 100644 index 06668501498..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionRestHookInterceptor.java +++ /dev/null @@ -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 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; - } - -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java index f155a961edf..87a617aad51 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/JpaConstants.java @@ -23,51 +23,6 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.rest.api.Constants; public class JpaConstants { - - /** - *

- * This extension should be of type string and should be - * placed on the Subscription.channel element - *

- */ - public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from"; - - /** - *

- * This extension should be of type string and should be - * placed on the Subscription.channel element - *

- */ - 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. - *

- * This extension should be of type boolean and should be - * placed on the Subscription.channel element. - *

- */ - 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. - *

- * Note that if the resource is now deleted, this may cause - * the delivery to be cancelled altogether. - *

- * - *

- * This extension should be of type boolean and should be - * placed on the Subscription.channel element. - *

- */ - 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 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/StoppableSubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/StoppableSubscriptionDeliveringRestHookSubscriber.java new file mode 100644 index 00000000000..d4603c46202 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/StoppableSubscriptionDeliveringRestHookSubscriber.java @@ -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; + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java index 545962d4246..ed7243b9b9e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java @@ -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) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java index dfceb79fc87..7e9574c71ff 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestJPAConfig.java @@ -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(); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d92b393293d..e24e7178622 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index aa28e56d77d..d08e7b3b9cc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -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 myValueSetDao; @Autowired private ISearchParamRegistry mySearchParamRegistry; + @Autowired + protected SubscriptionLoader mySubscriptionLoader; @Before public void beforeCreateInterceptor() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InvalidSubscriptionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InvalidSubscriptionTest.java index b958190e5f6..73b444b906e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InvalidSubscriptionTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3InvalidSubscriptionTest.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index b07b4b3f281..e88997bfb2a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -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 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() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java index 230c01712fc..57553c26da6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoCreatePlaceholdersR4Test.java @@ -1,23 +1,26 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.junit.*; +import org.hl7.fhir.r4.model.Task; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; -import java.util.*; +import java.util.List; -import static org.apache.commons.lang3.StringUtils.defaultString; import static org.hamcrest.Matchers.contains; import static org.junit.Assert.*; -import static org.mockito.Matchers.eq; -@SuppressWarnings({ "unchecked", "deprecation" }) +@SuppressWarnings({"unchecked", "deprecation"}) public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoCreatePlaceholdersR4Test.class); @@ -25,6 +28,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { @After public final void afterResetDao() { myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); } @Test @@ -97,7 +101,7 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { } @Test - public void testUpdateWithBadReferenceIsPermitted() { + public void testUpdateWithBadReferenceIsPermittedAlphanumeric() { assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); @@ -105,11 +109,49 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test { o.setStatus(ObservationStatus.FINAL); IIdType id = myObservationDao.create(o, mySrd).getId(); + try { + myPatientDao.read(new IdType("Patient/FOO")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + o = new Observation(); o.setId(id); o.setStatus(ObservationStatus.FINAL); o.getSubject().setReference("Patient/FOO"); myObservationDao.update(o, mySrd); + + myPatientDao.read(new IdType("Patient/FOO")); + + } + + @Test + public void testUpdateWithBadReferenceIsPermittedNumeric() { + assertFalse(myDaoConfig.isAutoCreatePlaceholderReferenceTargets()); + myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true); + myDaoConfig.setResourceClientIdStrategy(DaoConfig.ClientIdStrategyEnum.ANY); + + Observation o = new Observation(); + o.setStatus(ObservationStatus.FINAL); + IIdType id = myObservationDao.create(o, mySrd).getId(); + + try { + myPatientDao.read(new IdType("Patient/999999999999999")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } + + o = new Observation(); + o.setId(id); + o.setStatus(ObservationStatus.FINAL); + o.getSubject().setReference("Patient/999999999999999"); + myObservationDao.update(o, mySrd); + + + myPatientDao.read(new IdType("Patient/999999999999999")); + } @AfterClass diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InvalidSubscriptionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InvalidSubscriptionTest.java index 9065285af1d..e8a1a7bbe8a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InvalidSubscriptionTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4InvalidSubscriptionTest.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index 66a932188fe..11a6830f2fa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -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 getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { return null; } + + @Override + public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { + // nothing + } }; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java index 0fff1fceb19..3e4e064616e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java index 62e2534f45d..f897c237199 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 8fe97c13e6d..7d5ec840386 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -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); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java index 051a72c7903..4b644ca3458 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java @@ -10,6 +10,8 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; +import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -19,13 +21,10 @@ import org.apache.http.entity.StringEntity; import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Reference; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; @@ -236,6 +235,32 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes } @Test + public void testCreateReflexResourceTheHardWay() throws IOException, ServletException { + ServerOperationInterceptorAdapter interceptor = new ReflexInterceptor(); + + ourRestServer.registerInterceptor(interceptor); + try { + + Patient p = new Patient(); + p.setActive(true); + IIdType pid = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); + + Bundle observations = ourClient + .search() + .forResource("Observation") + .where(Observation.SUBJECT.hasId(pid)) + .returnBundle(Bundle.class) + .execute(); + assertEquals(1, observations.getEntry().size()); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(observations)); + + } finally { + ourRestServer.unregisterInterceptor(interceptor); + } + } + + + @Test public void testCreateResourceWithVersionedReference() throws IOException, ServletException { String methodName = "testCreateResourceWithVersionedReference"; @@ -353,6 +378,26 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes } } + public class ReflexInterceptor extends ServerOperationInterceptorAdapter { + @Override + public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + if (theResource instanceof Patient) { + ((ServletRequestDetails) theRequest).getServletRequest().setAttribute("CREATED_PATIENT", theResource); + } + } + + @Override + public void processingCompletedNormally(ServletRequestDetails theRequestDetails) { + Patient createdPatient = (Patient) theRequestDetails.getServletRequest().getAttribute("CREATED_PATIENT"); + if (createdPatient != null) { + Observation observation = new Observation(); + observation.setSubject(new Reference(createdPatient.getId())); + + ourClient.create().resource(observation).execute(); + } + } + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 495ed9f54b8..c5dcf1d50cb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -2772,6 +2772,105 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { assertThat(actual.getText().getDiv().getValueAsString(), containsString("IdentifiertestSearchByResourceChain01")); } + @Test + public void testTerminologyWithCompleteCs_Expand() throws Exception { + + CodeSystem cs = new CodeSystem(); + cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + cs.setUrl("http://cs"); + CodeSystem.ConceptDefinitionComponent a = cs.addConcept() + .setCode("A"); + a.addConcept().setCode("A1"); + a.addConcept().setCode("A2"); + CodeSystem.ConceptDefinitionComponent b = cs.addConcept() + .setCode("B"); + b.addConcept().setCode("B1"); + b.addConcept().setCode("B2"); + ourClient.create().resource(cs).execute(); + + ValueSet vs = new ValueSet(); + vs.setUrl("http://vs"); + vs.getCompose() + .addInclude() + .setSystem("http://cs") + .addFilter() + .setProperty("concept") + .setOp(ValueSet.FilterOperator.ISA) + .setValue("A"); + IIdType vsid = ourClient.create().resource(vs).execute().getId().toUnqualifiedVersionless(); + + HttpGet read = new HttpGet(ourServerBase + "/" + vsid.getValue() + "/$expand"); + try (CloseableHttpResponse response = ourHttpClient.execute(read)) { + String text = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(text); + assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatusLine().getStatusCode()); + assertThat(text, containsString("\"A\"")); + assertThat(text, containsString("\"A1\"")); + assertThat(text, not(containsString("\"B\""))); + assertThat(text, not(containsString("\"B1\""))); + } + + +// HttpGet read = new HttpGet(ourServerBase + "/Observation?patient=P5000000302&_sort:desc=code&code:in=http://fkcfhir.org/fhir/vs/ccdacapddialysisorder"); +// try (CloseableHttpResponse response = ourHttpClient.execute(read)) { +// String text = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); +// ourLog.info(text); +// assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatusLine().getStatusCode()); +// assertThat(text, not(containsString("\"text\",\"type\""))); +// } + } + + @Test + public void testTerminologyWithCompleteCs_SearchForConceptIn() throws Exception { + + CodeSystem cs = new CodeSystem(); + cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + cs.setUrl("http://cs"); + CodeSystem.ConceptDefinitionComponent a = cs.addConcept() + .setCode("A"); + a.addConcept().setCode("A1"); + a.addConcept().setCode("A2"); + CodeSystem.ConceptDefinitionComponent b = cs.addConcept() + .setCode("B"); + b.addConcept().setCode("B1"); + b.addConcept().setCode("B2"); + ourClient.create().resource(cs).execute(); + + ValueSet vs = new ValueSet(); + vs.setUrl("http://vs"); + vs.getCompose() + .addInclude() + .setSystem("http://cs") + .addFilter() + .setProperty("concept") + .setOp(ValueSet.FilterOperator.ISA) + .setValue("A"); + ourClient.create().resource(vs).execute().getId().toUnqualifiedVersionless(); + + Observation obs = new Observation(); + obs.getCode().addCoding().setSystem("http://cs").setCode("A1"); + obs.setValue(new StringType("OBS1")); + obs.setStatus(ObservationStatus.FINAL); + ourClient.create().resource(obs).execute(); + + Observation obs2 = new Observation(); + obs2.getCode().addCoding().setSystem("http://cs").setCode("B1"); + obs2.setStatus(ObservationStatus.FINAL); + obs2.setValue(new StringType("OBS2")); + ourClient.create().resource(obs2).execute(); + + HttpGet read = new HttpGet(ourServerBase + "/Observation?code:in=http://vs"); + try (CloseableHttpResponse response = ourHttpClient.execute(read)) { + String text = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(text); + assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatusLine().getStatusCode()); + assertThat(text, containsString("\"OBS1\"")); + assertThat(text, not(containsString("\"OBS2\""))); + } + + + } + @Test public void testSearchBundleDoesntIncludeTextElement() throws Exception { HttpGet read = new HttpGet(ourServerBase + "/Patient?_format=json"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java index 1e7152417b6..a11ad70f804 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImplTest.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceReindexJobDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -63,6 +64,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { @Captor private ArgumentCaptor myHighCaptor; private ResourceReindexJobEntity mySingleJob; + @Mock + private ISearchParamRegistry mySearchParamRegistry; @Override protected FhirContext getContext() { @@ -87,6 +90,7 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { mySvc.setReindexJobDaoForUnitTest(myReindexJobDao); mySvc.setResourceTableDaoForUnitTest(myResourceTableDao); mySvc.setTxManagerForUnitTest(myTxManager); + mySvc.setSearchParamRegistryForUnitTest(mySearchParamRegistry); mySvc.start(); } @@ -175,6 +179,8 @@ public class ResourceReindexingSvcImplTest extends BaseJpaTest { verify(myReindexJobDao, times(1)).getReindexCount(any()); verify(myReindexJobDao, times(1)).setReindexCount(any(), anyInt()); verifyNoMoreInteractions(myReindexJobDao); + + verify(mySearchParamRegistry, times(1)).forceRefresh(); } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/BaseSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/BaseSubscriptionsR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java index 04f6a800449..0085e922499 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/BaseSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java @@ -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; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/CountingInterceptor.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/CountingInterceptor.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/CountingInterceptor.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/CountingInterceptor.java index 287c3c8b898..ab63d835282 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/CountingInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/CountingInterceptor.java @@ -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; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java similarity index 84% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java index f75f67152f5..6af748eda5d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirClientSearchParamProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java @@ -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); } - } - - } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSubscriptionProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSubscriptionProviderTest.java new file mode 100644 index 00000000000..5d23b900dc1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSubscriptionProviderTest.java @@ -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 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)); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirR4Util.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirR4Util.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirR4Util.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirR4Util.java index a2b085d7174..9bad30dd6ae 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/FhirR4Util.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirR4Util.java @@ -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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java new file mode 100644 index 00000000000..f2347c48e87 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTestUtil.java @@ -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; + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu2Test.java similarity index 73% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu2Test.java index a722b4ac937..582ad7aaa57 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu2Test.java @@ -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 mySubscriptionIds = new ArrayList<>(); @Autowired - private SubscriptionEmailInterceptor mySubscriber; - @Autowired - private List> 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"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java index 4035b3a9ed3..02705f1d770 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/EmailSubscriptionDstu3Test.java @@ -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 ourCreatedObservations = Lists.newArrayList(); private static int ourListenerPort; private static List 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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSenderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSenderTest.java index 828441acb29..6c6b8a6cd59 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSenderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSenderTest.java @@ -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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java index 943987dfc5b..c4ece8f0207 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR4.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java similarity index 87% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java index 8d0c3a44baa..8d34fd1f8c2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookActivatesPreExistingSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java @@ -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 ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private static List 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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java index bfe49f57518..2a79125a84e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java @@ -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 ourUpdatedObservations = Lists.newArrayList(); private List mySubscriptionIds = new ArrayList(); + @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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java index fcaa6964165..97716aac54f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java @@ -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 ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private List 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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java similarity index 78% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 81ec08ec3c7..b6ae76bc815 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java index c542bb0f9ab..599ee55a986 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -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 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(); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java index 5064960ee91..889a723d4eb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test.java @@ -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 ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + @Autowired + private SubscriptionTestUtil mySubscriptionTestUtil; + @Override protected boolean shouldLogClient() { return false; @@ -50,21 +54,21 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu3Test extends B ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); 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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java index d1f95a93a7d..56782fbabef 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigR4Test.java @@ -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 ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + @Autowired + private SubscriptionTestUtil mySubscriptionTestUtil; + @Override protected boolean shouldLogClient() { return false; @@ -49,13 +54,13 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base ourClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); 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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java similarity index 90% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java index 8e95fa6506f..dc23c1cf23e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookWithEventDefinitionR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithEventDefinitionR4Test.java @@ -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 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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java index e36eace4790..e1efd7e5659 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionTriggeringDstu3Test.java @@ -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 ourContentTypes = new ArrayList<>(); private List 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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu2Test.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu2Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu2Test.java index 492398b6fab..54e1f616fff 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu2Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu3Test.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu3Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu3Test.java index 9d90b49b931..b213c167654 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithCriteriaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaDstu3Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithCriteriaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaR4Test.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithCriteriaR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaR4Test.java index dfe9b283c9f..ad34b30e238 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithCriteriaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithCriteriaR4Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu2Test.java similarity index 92% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu2Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu2Test.java index 7ba7a7d8366..4997528a8e5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu2Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java similarity index 91% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu3Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java index 6ce01a57b2c..5ac36a04baa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/WebsocketWithSubscriptionIdDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdDstu3Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithSubscriptionIdR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR4Test.java similarity index 91% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithSubscriptionIdR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR4Test.java index a815ff10f58..374b72efa37 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/WebsocketWithSubscriptionIdR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR4Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java index cbaa7c6fbd2..e7006342458 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu3Test.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; +import com.google.common.collect.Lists; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode; @@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -583,6 +585,50 @@ public class TerminologySvcImplDstu3Test extends BaseJpaDstu3Test { } } + /** + * Check that a custom ValueSet against a custom CodeSystem expands correctly + */ + @Test + public void testCustomValueSetExpansion() { + + CodeSystem cs= new CodeSystem(); + cs.setUrl("http://codesystems-r-us"); + cs.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT); + IIdType csId = myCodeSystemDao.create(cs).getId().toUnqualifiedVersionless(); + + TermCodeSystemVersion version = new TermCodeSystemVersion(); + version.getConcepts().add(new TermConcept(version, "A")); + version.getConcepts().add(new TermConcept(version, "B")); + version.getConcepts().add(new TermConcept(version, "C")); + version.getConcepts().add(new TermConcept(version, "D")); + runInTransaction(()->{ + ResourceTable resTable = myEntityManager.find(ResourceTable.class, csId.getIdPartAsLong()); + version.setResource(resTable); + myTermSvc.storeNewCodeSystemVersion(csId.getIdPartAsLong(), cs.getUrl(), "My System", version); + }); + + org.hl7.fhir.dstu3.model.ValueSet vs = new org.hl7.fhir.dstu3.model.ValueSet(); + vs.setUrl("http://valuesets-r-us"); + vs.getCompose() + .addInclude() + .setSystem(cs.getUrl()) + .addConcept(new org.hl7.fhir.dstu3.model.ValueSet.ConceptReferenceComponent().setCode("A")) + .addConcept(new org.hl7.fhir.dstu3.model.ValueSet.ConceptReferenceComponent().setCode("C")); + myValueSetDao.create(vs); + + org.hl7.fhir.dstu3.model.ValueSet expansion = myValueSetDao.expandByIdentifier(vs.getUrl(), null); + List expansionCodes = expansion + .getExpansion() + .getContains() + .stream() + .map(t -> t.getCode()) + .sorted() + .collect(Collectors.toList()); + assertEquals(Lists.newArrayList("A","C"), expansionCodes); + + } + + public static List toCodesContains(List theContains) { List retVal = new ArrayList<>(); diff --git a/hapi-fhir-jpaserver-example/README.md b/hapi-fhir-jpaserver-example/README.md index 8339170e048..5c37386c841 100644 --- a/hapi-fhir-jpaserver-example/README.md +++ b/hapi-fhir-jpaserver-example/README.md @@ -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. diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 5a2e5fad948..38506ddd976 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -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 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 diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java index 3147a776819..3c802e6cb10 100644 --- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java @@ -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 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 diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java index 7cbb219d533..c8b659e1da3 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/JdbcUtils.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.migrate; * 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. @@ -22,10 +22,19 @@ package ca.uhn.fhir.jpa.migrate; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTableColumnTypeTask; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hibernate.boot.model.naming.Identifier; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; import org.hibernate.engine.jdbc.dialect.spi.DialectResolver; +import org.hibernate.engine.jdbc.env.internal.NormalizingIdentifierHelperImpl; +import org.hibernate.engine.jdbc.env.spi.*; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; +import org.hibernate.engine.jdbc.spi.TypeInfo; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.SequenceInformation; +import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.ColumnMapRowMapper; @@ -53,7 +62,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, true); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), false, true); Set indexNames = new HashSet<>(); while (indexes.next()) { @@ -81,7 +90,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), theTableName, false, false); + ResultSet indexes = metadata.getIndexInfo(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), false, false); while (indexes.next()) { String indexName = indexes.getString("INDEX_NAME"); @@ -112,7 +121,7 @@ public class JdbcUtils { metadata = connection.getMetaData(); String catalog = connection.getCatalog(); String schema = connection.getSchema(); - ResultSet indexes = metadata.getColumns(catalog, schema, theTableName, null); + ResultSet indexes = metadata.getColumns(catalog, schema, massageIdentifier(metadata, theTableName), null); while (indexes.next()) { @@ -165,7 +174,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), theTableName, connection.getCatalog(), connection.getSchema(), theForeignTable); + ResultSet indexes = metadata.getCrossReference(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theForeignTable)); Set columnNames = new HashSet<>(); while (indexes.next()) { @@ -201,7 +210,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, null); + ResultSet indexes = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null); Set columnNames = new HashSet<>(); while (indexes.next()) { @@ -233,27 +242,83 @@ public class JdbcUtils { Set sequenceNames = new HashSet<>(); if (dialect.supportsSequences()) { - String sql = dialect.getQuerySequencesString(); - if (sql != null) { - Statement statement = null; - ResultSet rs = null; - try { - statement = connection.createStatement(); - rs = statement.executeQuery(sql); - - while (rs.next()) { - sequenceNames.add(rs.getString(1).toUpperCase()); - } - } finally { - if (rs != null) rs.close(); - if (statement != null) statement.close(); + // Use Hibernate to get a list of current sequences + SequenceInformationExtractor sequenceInformationExtractor = dialect.getSequenceInformationExtractor(); + ExtractionContext extractionContext = new ExtractionContext.EmptyExtractionContext() { + @Override + public Connection getJdbcConnection() { + return connection; } + @Override + public ServiceRegistry getServiceRegistry() { + return super.getServiceRegistry(); + } + + @Override + public JdbcEnvironment getJdbcEnvironment() { + return new JdbcEnvironment() { + @Override + public Dialect getDialect() { + return dialect; + } + + @Override + public ExtractedDatabaseMetaData getExtractedDatabaseMetaData() { + return null; + } + + @Override + public Identifier getCurrentCatalog() { + return null; + } + + @Override + public Identifier getCurrentSchema() { + return null; + } + + @Override + public QualifiedObjectNameFormatter getQualifiedObjectNameFormatter() { + return null; + } + + @Override + public IdentifierHelper getIdentifierHelper() { + return new NormalizingIdentifierHelperImpl(this, null, true, true, true, null, null, null); + } + + @Override + public NameQualifierSupport getNameQualifierSupport() { + return null; + } + + @Override + public SqlExceptionHelper getSqlExceptionHelper() { + return null; + } + + @Override + public LobCreatorBuilder getLobCreatorBuilder() { + return null; + } + + @Override + public TypeInfo getTypeInfoForJdbcCode(int jdbcTypeCode) { + return null; + } + }; + } + }; + Iterable sequences = sequenceInformationExtractor.extractMetadata(extractionContext); + for (SequenceInformation next : sequences) { + sequenceNames.add(next.getSequenceName().getSequenceName().getText()); } + } return sequenceNames; - } catch (SQLException e ) { + } catch (SQLException e) { throw new InternalErrorException(e); } }); @@ -298,7 +363,7 @@ public class JdbcUtils { DatabaseMetaData metadata; try { metadata = connection.getMetaData(); - ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), theTableName, theColumnName); + ResultSet tables = metadata.getColumns(connection.getCatalog(), connection.getSchema(), massageIdentifier(metadata, theTableName), null); while (tables.next()) { String tableName = toUpperCase(tables.getString("TABLE_NAME"), Locale.US); @@ -325,4 +390,14 @@ public class JdbcUtils { }); } } + + private static String massageIdentifier(DatabaseMetaData theMetadata, String theCatalog) throws SQLException { + String retVal = theCatalog; + if (theMetadata.storesLowerCaseIdentifiers()) { + retVal = retVal.toLowerCase(); + } else { + retVal = retVal.toUpperCase(); + } + return retVal; + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java index a863ac73d17..f3558eed294 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTableColumnTypeTask.java @@ -66,6 +66,13 @@ public abstract class BaseTableColumnTypeTask extends B setColumnType(ColumnTypeEnum.DATE_TIMESTAMP, DriverTypeEnum.MSSQL_2012, "datetime2"); setColumnType(ColumnTypeEnum.DATE_TIMESTAMP, DriverTypeEnum.ORACLE_12C, "timestamp"); setColumnType(ColumnTypeEnum.DATE_TIMESTAMP, DriverTypeEnum.POSTGRES_9_4, "timestamp"); + + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.DERBY_EMBEDDED, "boolean"); + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.MSSQL_2012, "bit"); + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.MARIADB_10_1, "bit"); + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.ORACLE_12C, "number(1,0)"); + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.POSTGRES_9_4, "boolean"); + setColumnType(ColumnTypeEnum.BOOLEAN, DriverTypeEnum.MYSQL_5_7, "bit"); } public ColumnTypeEnum getColumnType() { @@ -157,6 +164,13 @@ public abstract class BaseTableColumnTypeTask extends B return "timestamp"; } }, + BOOLEAN { + @Override + public String getDescriptor(Long theColumnLength) { + Assert.isTrue(theColumnLength == null, "Must not supply a column length"); + return "boolean"; + } + }, INT { @Override public String getDescriptor(Long theColumnLength) { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 63534dad81d..63d60abd89e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -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 myTreatBaseUrlsAsLocal = new HashSet<>(); private Set myTreatReferencesAsLogical = new HashSet<>(DEFAULT_LOGICAL_BASE_URLS); private boolean myDefaultSearchParamsCanBeOverridden = false; + private Set 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 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 { } - } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 2093f66986c..e59ea7add71 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -610,12 +610,9 @@ public class ResourceTable extends BaseHasResource implements Serializable { if (myHasLinks && myResourceLinks != null) { myResourceLinksField = getResourceLinks() .stream() - .map(t->{ - Long retVal = t.getTargetResourcePid(); - return retVal; - }) + .map(ResourceLink::getTargetResourcePid) .filter(Objects::nonNull) - .map(t->t.toString()) + .map(Object::toString) .collect(Collectors.joining(" ")); } else { myResourceLinksField = null; diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index c5ee9d19bfd..51ddbfd0657 100644 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -82,6 +82,10 @@ hapi-fhir-validation-resources-r4 ${project.version}
+ + javax.annotation + javax.annotation-api + diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java index 72e35d1db1e..a6b1e6d34ab 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java @@ -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(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java index 737b23919c6..b9f658d22a7 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java @@ -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,30 +44,20 @@ import java.util.*; import static org.apache.commons.lang3.StringUtils.isBlank; public abstract class BaseSearchParamRegistry 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> myBuiltInSearchParams; private volatile Map> myActiveUniqueSearchParams = Collections.emptyMap(); private volatile Map, List>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap(); - @Autowired - private FhirContext myCtx; private volatile Map> myActiveSearchParams; - @Autowired - private ModelConfig myModelConfig; private volatile long myLastRefresh; - private ApplicationContext myApplicationContext; - private ISearchParamProvider mySearchParamProvider; - - public BaseSearchParamRegistry(ISearchParamProvider theSearchParamProvider) { - super(); - mySearchParamProvider = theSearchParamProvider; - } - - @VisibleForTesting - public void setSearchParamProvider(ISearchParamProvider theSearchParamProvider) { - mySearchParamProvider = theSearchParamProvider; - } @Override public void requestRefresh() { @@ -128,7 +117,7 @@ public abstract class BaseSearchParamRegistry implemen return Collections.unmodifiableList(retVal); } - public Map> getBuiltInSearchParams() { + private Map> getBuiltInSearchParams() { return myBuiltInSearchParams; } @@ -220,10 +209,10 @@ public abstract class BaseSearchParamRegistry implemen public void postConstruct() { Map> resourceNameToSearchParams = new HashMap<>(); - Set resourceNames = myCtx.getResourceNames(); + Set resourceNames = myFhirContext.getResourceNames(); for (String resourceName : resourceNames) { - RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(resourceName); + RuntimeResourceDefinition nextResDef = myFhirContext.getResourceDefinition(resourceName); String nextResourceName = nextResDef.getName(); HashMap nameToParam = new HashMap<>(); resourceNameToSearchParams.put(nextResourceName, nameToParam); @@ -284,7 +273,7 @@ public abstract class BaseSearchParamRegistry implemen continue; } - for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myCtx, nextSp)) { + for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, nextSp)) { if (isBlank(nextBaseName)) { continue; } @@ -349,5 +338,9 @@ public abstract class BaseSearchParamRegistry implemen return getActiveSearchParams(theResourceDef.getName()).values(); } - + @VisibleForTesting + @Override + public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { + mySearchParamProvider = theSearchParamProvider; + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java index 9d44ed27be5..dc3d021dd87 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java @@ -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 getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef); + + @VisibleForTesting + void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java index 20d7bf3d6a4..3dfb2ffec51 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu2.java @@ -39,10 +39,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryDstu2 extends BaseSearchParamRegistry { -public SearchParamRegistryDstu2(ISearchParamProvider theSearchParamProvider) { - super(theSearchParamProvider); - } - @Override protected JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) { String name = theNextSp.getCode(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java index cb4e6c065aa..192bcd36e8e 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryDstu3.java @@ -39,10 +39,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryDstu3 extends BaseSearchParamRegistry { - public SearchParamRegistryDstu3(ISearchParamProvider theSearchParamProvider) { - super(theSearchParamProvider); - } - @Override protected JpaRuntimeSearchParam toRuntimeSp(SearchParameter theNextSp) { String name = theNextSp.getCode(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java index 6ecdd680093..3198a9fbf70 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryR4.java @@ -41,10 +41,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class SearchParamRegistryR4 extends BaseSearchParamRegistry { - 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 getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { return null; } + + @Override + public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { + // nothing + } }; SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 5dfb07771a6..49c0b24cfbf 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -29,6 +29,47 @@ hapi-fhir-validation ${project.version} + + org.springframework + spring-messaging + + + org.springframework + spring-context + + + xml-apis + xml-apis + + + + + org.thymeleaf + thymeleaf + + + org.thymeleaf + thymeleaf-spring5 + + + org.springframework + spring-websocket + + + org.springframework + spring-context-support + + + javax.mail + javax.mail-api + provided + + + javax.servlet + javax.servlet-api + provided + + @@ -46,6 +87,16 @@ mockito-core test + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/CanonicalSubscription.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index d1c561db64c..f63a3fa0983 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -54,7 +54,7 @@ public class CanonicalSubscription implements Serializable { @JsonProperty("headers") private List 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; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscriptionChannelType.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscriptionChannelType.java new file mode 100644 index 00000000000..eaffe000315 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscriptionChannelType.java @@ -0,0 +1,135 @@ +package ca.uhn.fhir.jpa.subscription.module; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 "?"; + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/LinkedBlockingQueueSubscriptionChannel.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/LinkedBlockingQueueSubscriptionChannel.java new file mode 100644 index 00000000000..d2ba6986278 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/LinkedBlockingQueueSubscriptionChannel.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.jpa.subscription.module; + +/*- + * #%L + * HAPI FHIR Subscription 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.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); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java similarity index 89% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index c02bae8cf16..4560750cf2a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -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; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionChannel.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionChannel.java new file mode 100644 index 00000000000..055bf3fd785 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/SubscriptionChannel.java @@ -0,0 +1,103 @@ +package ca.uhn.fhir.jpa.subscription.module; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 myQueue; + + public SubscriptionChannel(BlockingQueue 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(); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscription.java new file mode 100644 index 00000000000..c16ee936082 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscription.java @@ -0,0 +1,93 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 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(); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java new file mode 100644 index 00000000000..93745ebc6e7 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java @@ -0,0 +1,72 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 myCache = new ConcurrentHashMap<>(); + + public ActiveSubscription get(String theIdPart) { + return myCache.get(theIdPart); + } + + public Collection 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 theAllIds) { + for (String next : new ArrayList<>(myCache.keySet())) { + if (!theAllIds.contains(next)) { + ourLog.info("Unregistering Subscription/{}", next); + remove(next); + } + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/BlockingQueueSubscriptionChannelFactory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/BlockingQueueSubscriptionChannelFactory.java new file mode 100644 index 00000000000..6abeda02c3f --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/BlockingQueueSubscriptionChannelFactory.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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"); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionChannelFactory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionChannelFactory.java new file mode 100644 index 00000000000..e514906f3e0 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionChannelFactory.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.messaging.SubscribableChannel; + +public interface ISubscriptionChannelFactory { + SubscribableChannel newDeliveryChannel(String theSubscriptionId, String theChannelType); + + SubscribableChannel newMatchingChannel(String theChannelName); +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionProvider.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionProvider.java new file mode 100644 index 00000000000..49e576cde64 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ISubscriptionProvider.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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); +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCannonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCannonicalizer.java new file mode 100644 index 00000000000..80a2284666f --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCannonicalizer.java @@ -0,0 +1,169 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.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 { + @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 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; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java new file mode 100644 index 00000000000..0c7461a5e3d --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java @@ -0,0 +1,90 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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 class SubscriptionConstants { + + /** + *

+ * This extension should be of type string and should be + * placed on the Subscription.channel element + *

+ */ + public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from"; + + /** + *

+ * This extension should be of type string and should be + * placed on the Subscription.channel element + *

+ */ + 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. + *

+ * This extension should be of type boolean and should be + * placed on the Subscription.channel element. + *

+ */ + 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. + *

+ * Note that if the resource is now deleted, this may cause + * the delivery to be cancelled altogether. + *

+ * + *

+ * This extension should be of type boolean and should be + * placed on the Subscription.channel element. + *

+ */ + 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; +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionDeliveryHandlerFactory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionDeliveryHandlerFactory.java new file mode 100644 index 00000000000..9e2fcc32382 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionDeliveryHandlerFactory.java @@ -0,0 +1,57 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 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; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java new file mode 100644 index 00000000000..6626d0a3b8b --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java @@ -0,0 +1,123 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.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 resourceList = subscriptionBundleList.getResources(0, subscriptionBundleList.size()); + + Set 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; + } +} + diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java new file mode 100644 index 00000000000..9b170b1dbd6 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -0,0 +1,136 @@ +package ca.uhn.fhir.jpa.subscription.module.cache; + +/*- + * #%L + * HAPI FHIR Subscription 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.module.CanonicalSubscription; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +/** + * + * Cache of active subscriptions. When a new subscription is added to the cache, a new Spring Channel is created + * and a new MessageHandler for that subscription is subscribed to that channel. These subscriptions, channels, and + * handlers are all caches in this registry so they can be removed it the subscription is deleted. + */ + +@Component +public class SubscriptionRegistry { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionRegistry.class); + + @Autowired + SubscriptionCannonicalizer mySubscriptionCanonicalizer; + @Autowired + SubscriptionDeliveryHandlerFactory mySubscriptionDeliveryHandlerFactory; + @Autowired + ISubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; + + private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); + + public ActiveSubscription get(String theIdPart) { + return myActiveSubscriptionCache.get(theIdPart); + } + + public Collection getAll() { + return myActiveSubscriptionCache.getAll(); + } + + private Optional hasSubscription(IIdType theId) { + Validate.notNull(theId); + Validate.notBlank(theId.getIdPart()); + Optional activeSubscription = Optional.ofNullable(myActiveSubscriptionCache.get(theId.getIdPart())); + return activeSubscription.map(ActiveSubscription::getSubscription); + } + + @SuppressWarnings("UnusedReturnValue") + public CanonicalSubscription registerSubscription(IIdType theId, IBaseResource theSubscription) { + Validate.notNull(theId); + String subscriptionId = theId.getIdPart(); + Validate.notBlank(subscriptionId); + Validate.notNull(theSubscription); + + CanonicalSubscription canonicalized = mySubscriptionCanonicalizer.canonicalize(theSubscription); + SubscribableChannel deliveryChannel = mySubscriptionDeliveryChannelFactory.newDeliveryChannel(subscriptionId, canonicalized.getChannelType().toCode().toLowerCase()); + Optional deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(canonicalized); + + ActiveSubscription activeSubscription = new ActiveSubscription(canonicalized, deliveryChannel); + myActiveSubscriptionCache.put(subscriptionId, activeSubscription); + + deliveryHandler.ifPresent(handler -> activeSubscription.register(handler)); + + return canonicalized; + } + + public void unregisterSubscription(IIdType theId) { + Validate.notNull(theId); + String subscriptionId = theId.getIdPart(); + myActiveSubscriptionCache.remove(subscriptionId); + } + + @PreDestroy + public void preDestroy() { + unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); + } + + public void unregisterAllSubscriptionsNotInCollection(Collection theAllIds) { + myActiveSubscriptionCache.unregisterAllSubscriptionsNotInCollection(theAllIds); + } + + public synchronized boolean registerSubscriptionUnlessAlreadyRegistered(IBaseResource theSubscription) { + Optional existingSubscription = hasSubscription(theSubscription.getIdElement()); + CanonicalSubscription newSubscription = mySubscriptionCanonicalizer.canonicalize(theSubscription); + + if (existingSubscription.isPresent()) { + if (newSubscription.equals(existingSubscription.get())) { + // No changes + return false; + } + ourLog.info("Updating already-registered active subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); + unregisterSubscription(theSubscription.getIdElement()); + } else { + ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); + } + registerSubscription(theSubscription.getIdElement(), theSubscription); + return true; + } + + public boolean unregisterSubscriptionIfRegistered(IBaseResource theSubscription, String theStatusString) { + if (hasSubscription(theSubscription.getIdElement()).isPresent()) { + ourLog.info("Removing {} subscription {}", theStatusString, theSubscription.getIdElement().toUnqualified().getValue()); + unregisterSubscription(theSubscription.getIdElement()); + return true; + } + return false; + } + + public int size() { + return myActiveSubscriptionCache.size(); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java similarity index 67% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java index 18548668e27..4e5fdab2e18 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.config; +package ca.uhn.fhir.jpa.subscription.module.config; /*- * #%L @@ -21,24 +21,19 @@ package ca.uhn.fhir.jpa.subscription.config; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; -import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import org.springframework.beans.factory.annotation.Autowired; +import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.module.cache.BlockingQueueSubscriptionChannelFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration -@ComponentScan(basePackages = "ca.uhn.fhir.jpa") +@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module"}) public abstract class BaseSubscriptionConfig { - @Autowired - IGenericClient myClient; - public abstract FhirContext fhirContext(); @Bean - protected ISearchParamProvider searchParamProvider() { - return new FhirClientSearchParamProvider(myClient); + public ISubscriptionChannelFactory blockingQueueSubscriptionDeliveryChannelFactory() { + return new BlockingQueueSubscriptionChannelFactory(); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/SubscriptionDstu3Config.java similarity index 92% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/SubscriptionDstu3Config.java index 939b92b0893..c1d2f05f21f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/config/BaseSubscriptionDstu3Config.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/SubscriptionDstu3Config.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.config; +package ca.uhn.fhir.jpa.subscription.module.config; /*- * #%L @@ -32,7 +32,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; // From BaseDstu3Config -public class BaseSubscriptionDstu3Config extends BaseSubscriptionConfig { +public class SubscriptionDstu3Config extends BaseSubscriptionConfig { @Override public FhirContext fhirContext() { return fhirContextDstu3(); @@ -52,7 +52,7 @@ public class BaseSubscriptionDstu3Config extends BaseSubscriptionConfig { @Bean public ISearchParamRegistry searchParamRegistry() { - return new SearchParamRegistryDstu3(searchParamProvider()); + return new SearchParamRegistryDstu3(); } @Bean(autowire = Autowire.BY_TYPE) diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java similarity index 99% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java index aa9d1fc5eaf..045db3507b8 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/CriteriaResourceMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; /*- * #%L diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java similarity index 86% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java index 9d645a5b719..d1060308531 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/ISubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; /*- * #%L @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.subscription.matcher; * #L% */ -import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; public interface ISubscriptionMatcher { SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java similarity index 90% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java index bb9005c6958..9688d4afa03 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemory.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; /*- * #%L @@ -26,16 +26,12 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; -import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -@Service -@Lazy -public class SubscriptionMatcherInMemory implements ISubscriptionMatcher { +public class InMemorySubscriptionMatcher implements ISubscriptionMatcher { @Autowired private FhirContext myContext; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InlineResourceLinkResolver.java similarity index 97% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InlineResourceLinkResolver.java index 992ab36a074..ac1e8406b7e 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/InlineResourceLinkResolver.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InlineResourceLinkResolver.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; /*- * #%L diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java similarity index 97% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java index 620116fc4d0..2bc970c559a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatchResult.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; /*- * #%L diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientResourceRetriever.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientResourceRetriever.java new file mode 100644 index 00000000000..9575c5a65a3 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientResourceRetriever.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +/*- + * #%L + * HAPI FHIR Subscription 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.subscription.module.subscriber.IResourceRetriever; +import ca.uhn.fhir.rest.client.api.IGenericClient; +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 FhirClientResourceRetriever implements IResourceRetriever { + private static final Logger ourLog = LoggerFactory.getLogger(FhirClientResourceRetriever.class); + + @Autowired + FhirContext myFhirContext; + @Autowired + IGenericClient myClient; + + @Override + public IBaseResource getResource(IIdType payloadId) throws ResourceGoneException { + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType()); + + return myClient.search().forResource(resourceDef.getName()).withIdAndCompartment(payloadId.getIdPart(), payloadId.getResourceType()).execute(); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java similarity index 84% rename from hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java index 50b1b5de76c..422d5fd8c1f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProvider.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.standalone; /*- * #%L @@ -32,11 +32,18 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.util.BundleUtil; import org.hl7.fhir.instance.model.api.IBaseBundle; 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.stereotype.Service; +@Service public class FhirClientSearchParamProvider implements ISearchParamProvider { + private static final Logger ourLog = LoggerFactory.getLogger(FhirClientSearchParamProvider.class); - private final IGenericClient myClient; + private IGenericClient myClient; + @Autowired public FhirClientSearchParamProvider(IGenericClient theClient) { myClient = theClient; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSubscriptionProvider.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSubscriptionProvider.java new file mode 100644 index 00000000000..de4a7c1d211 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSubscriptionProvider.java @@ -0,0 +1,71 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +/*- + * #%L + * HAPI FHIR Subscription 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.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import ca.uhn.fhir.util.BundleUtil; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class FhirClientSubscriptionProvider implements ISubscriptionProvider { + @Autowired + private FhirContext myFhirContext; + @Autowired + private SubscriptionRegistry mySubscriptionRegistry; + + IGenericClient myClient; + + @Autowired + public FhirClientSubscriptionProvider(IGenericClient theClient) { + myClient = theClient; + } + + @Override + public IBundleProvider search(SearchParameterMap theMap) { + FhirContext fhirContext = myClient.getFhirContext(); + + String searchURL = ResourceTypeEnum.SUBSCRIPTION.getCode() + theMap.toNormalizedQueryString(myFhirContext); + + IBaseBundle bundle = myClient + .search() + .byUrl(searchURL) + .cacheControl(new CacheControlDirective().setNoCache(true)) + .execute(); + + return new SimpleBundleProvider(BundleUtil.toListOfResources(fhirContext, bundle)); + } + + @Override + public boolean loadSubscription(IBaseResource theResource) { + return mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(theResource); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/StandaloneSubscriptionMessageHandler.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/StandaloneSubscriptionMessageHandler.java new file mode 100644 index 00000000000..35877ac242f --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/StandaloneSubscriptionMessageHandler.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +/*- + * #%L + * HAPI FHIR Subscription 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.subscription.module.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; +import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; +import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionCheckingSubscriber; +import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; +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.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; +import org.springframework.stereotype.Service; + +@Service +public class StandaloneSubscriptionMessageHandler implements MessageHandler { + private static final Logger ourLog = LoggerFactory.getLogger(StandaloneSubscriptionMessageHandler.class); + + @Autowired + FhirContext myFhirContext; + @Autowired + SubscriptionCheckingSubscriber mySubscriptionCheckingSubscriber; + @Autowired + SubscriptionRegistry mySubscriptionRegistry; + + @Override + public void handleMessage(Message theMessage) throws MessagingException { + if (!(theMessage instanceof ResourceModifiedJsonMessage)) { + ourLog.warn("Unexpected message payload type: {}", theMessage); + return; + } + ResourceModifiedMessage resourceModifiedMessage = ((ResourceModifiedJsonMessage) theMessage).getPayload(); + IBaseResource resource = resourceModifiedMessage.getNewPayload(myFhirContext); + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(resource); + + if (resourceDef.getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { + mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource); + } + mySubscriptionCheckingSubscriber.handleMessage(theMessage); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseJsonMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseJsonMessage.java similarity index 94% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseJsonMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseJsonMessage.java index b70157ef1ff..4608cbe4b19 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseJsonMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseJsonMessage.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseSubscriptionDeliverySubscriber.java similarity index 61% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseSubscriptionDeliverySubscriber.java index e6bc4412614..2aa8caf1638 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDeliverySubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/BaseSubscriptionDeliverySubscriber.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,23 +20,26 @@ package ca.uhn.fhir.jpa.subscription; * #L% */ -import org.hl7.fhir.r4.model.Subscription; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Scope; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; -import org.springframework.stereotype.Component; -public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptionSubscriber { +public abstract class BaseSubscriptionDeliverySubscriber implements MessageHandler { private static final Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionDeliverySubscriber.class); - public BaseSubscriptionDeliverySubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - super(theChannelType, theSubscriptionInterceptor); - } + @Autowired + protected FhirContext myFhirContext; + @Autowired + protected SubscriptionRegistry mySubscriptionRegistry; @Override - public void handleMessage(Message theMessage) throws MessagingException { + public void handleMessage(Message theMessage) throws MessagingException { if (!(theMessage.getPayload() instanceof ResourceDeliveryMessage)) { ourLog.warn("Unexpected payload type: {}", theMessage.getPayload()); return; @@ -46,15 +49,11 @@ public abstract class BaseSubscriptionDeliverySubscriber extends BaseSubscriptio try { ResourceDeliveryMessage msg = (ResourceDeliveryMessage) theMessage.getPayload(); - subscriptionId = msg.getSubscription().getIdElement(getContext()).getValue(); + subscriptionId = msg.getSubscription().getIdElement(myFhirContext).getValue(); - CanonicalSubscription updatedSubscription = (CanonicalSubscription) getSubscriptionInterceptor().getIdToSubscription().get(msg.getSubscription().getIdElement(getContext()).getIdPart()); + ActiveSubscription updatedSubscription = mySubscriptionRegistry.get(msg.getSubscription().getIdElement(myFhirContext).getIdPart()); if (updatedSubscription != null) { - msg.setSubscription(updatedSubscription); - } - - if (!subscriptionTypeApplies(msg.getSubscription())) { - return; + msg.setSubscription(updatedSubscription.getSubscription()); } handleMessage(msg); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/ISubscriptionWebsocketHandler.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/IResourceRetriever.java similarity index 70% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/ISubscriptionWebsocketHandler.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/IResourceRetriever.java index 81af49b16ed..cab184973cc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/ISubscriptionWebsocketHandler.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/IResourceRetriever.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.websocket; +package ca.uhn.fhir.jpa.subscription.module.subscriber; -/* +/*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,8 +20,9 @@ package ca.uhn.fhir.jpa.subscription.websocket; * #L% */ -import org.springframework.web.socket.WebSocketHandler; - -public interface ISubscriptionWebsocketHandler extends WebSocketHandler { +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +public interface IResourceRetriever { + IBaseResource getResource(IIdType id); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryJsonMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryJsonMessage.java similarity index 94% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryJsonMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryJsonMessage.java index 89bd4da618a..c0df987f3e1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryJsonMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryJsonMessage.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java index e4a495d4bc0..94bb7bb4244 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceDeliveryMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -21,9 +21,13 @@ package ca.uhn.fhir.jpa.subscription; */ import ca.uhn.fhir.context.FhirContext; -import com.fasterxml.jackson.annotation.*; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.Gson; -import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedJsonMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceModifiedJsonMessage.java similarity index 90% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedJsonMessage.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceModifiedJsonMessage.java index 6d6b8186089..9334b3d7bf3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedJsonMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceModifiedJsonMessage.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription; * #L% */ +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -28,7 +29,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class ResourceModifiedJsonMessage extends BaseJsonMessage { - @JsonProperty("headers") + @JsonProperty("payload") private ResourceModifiedMessage myPayload; /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriber.java similarity index 55% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriber.java index 480759e0f34..0e830891f7f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionCheckingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriber.java @@ -1,33 +1,28 @@ -package ca.uhn.fhir.jpa.subscription; +package ca.uhn.fhir.jpa.subscription.module.subscriber; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -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.matcher.ISubscriptionMatcher; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; +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.matcher.ISubscriptionMatcher; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Scope; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; -import java.util.List; +import java.util.Collection; import static org.apache.commons.lang3.StringUtils.isNotBlank; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -45,20 +40,16 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ -@Component -@Scope("prototype") -public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { +@Service +public class SubscriptionCheckingSubscriber implements MessageHandler { private Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriber.class); - private final ISubscriptionMatcher mySubscriptionMatcher; - @Autowired - private MatchUrlService myMatchUrlService; - - public SubscriptionCheckingSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor, ISubscriptionMatcher theSubscriptionMatcher) { - super(theChannelType, theSubscriptionInterceptor); - this.mySubscriptionMatcher = theSubscriptionMatcher; - } + private ISubscriptionMatcher mySubscriptionMatcher; + @Autowired + private FhirContext myFhirContext; + @Autowired + private SubscriptionRegistry mySubscriptionRegistry; @Override public void handleMessage(Message theMessage) throws MessagingException { @@ -82,17 +73,17 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { return; } - IIdType id = msg.getId(getContext()); + IIdType id = msg.getId(myFhirContext); String resourceType = id.getResourceType(); - List subscriptions = getSubscriptionInterceptor().getRegisteredSubscriptions(); + Collection subscriptions = mySubscriptionRegistry.getAll(); ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size()); - for (CanonicalSubscription nextSubscription : subscriptions) { + for (ActiveSubscription nextActiveSubscription : subscriptions) { - String nextSubscriptionId = nextSubscription.getIdElement(getContext()).toUnqualifiedVersionless().getValue(); - String nextCriteriaString = nextSubscription.getCriteriaString(); + String nextSubscriptionId = nextActiveSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue(); + String nextCriteriaString = nextActiveSubscription.getCriteriaString(); if (isNotBlank(msg.getSubscriptionId())) { if (!msg.getSubscriptionId().equals(nextSubscriptionId)) { @@ -125,45 +116,18 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber { ourLog.debug("Found match: queueing rest-hook notification for resource: {}", id.toUnqualifiedVersionless().getValue()); ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage(); - deliveryMsg.setPayload(getContext(), msg.getNewPayload(getContext())); - deliveryMsg.setSubscription(nextSubscription); + deliveryMsg.setPayload(myFhirContext, msg.getNewPayload(myFhirContext)); + deliveryMsg.setSubscription(nextActiveSubscription.getSubscription()); deliveryMsg.setOperationType(msg.getOperationType()); - deliveryMsg.setPayloadId(msg.getId(getContext())); + deliveryMsg.setPayloadId(msg.getId(myFhirContext)); ResourceDeliveryJsonMessage wrappedMsg = new ResourceDeliveryJsonMessage(deliveryMsg); - MessageChannel deliveryChannel = getSubscriptionInterceptor().getDeliveryChannel(nextSubscription); + MessageChannel deliveryChannel = nextActiveSubscription.getSubscribableChannel(); if (deliveryChannel != null) { deliveryChannel.send(wrappedMsg); } else { - ourLog.warn("Do not have deliovery channel for subscription {}", nextSubscription.getIdElement(getContext())); + ourLog.warn("Do not have deliovery channel for subscription {}", nextActiveSubscription.getIdElement(myFhirContext)); } } - - } - - /** - * Subclasses may override - */ - protected String massageCriteria(String theCriteria) { - return theCriteria; - } - - /** - * Search based on a query criteria - */ - protected IBundleProvider performSearch(String theCriteria) { - RuntimeResourceDefinition responseResourceDef = getSubscriptionDao().validateCriteriaAndReturnResourceDefinition(theCriteria); - SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); - - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); - - IFhirResourceDao responseDao = getSubscriptionInterceptor().getDao(responseResourceDef.getImplementingClass()); - responseCriteriaUrl.setLoadSynchronousUpTo(1); - - IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); - return responseResults; - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java similarity index 79% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index dd34f1d5abd..ab635443f35 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/resthook/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.resthook; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,13 +20,7 @@ package ca.uhn.fhir.jpa.subscription.resthook; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; -import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; -import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -36,9 +30,9 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.messaging.MessagingException; import org.springframework.stereotype.Component; @@ -56,9 +50,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDeliverySubscriber { private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); - public SubscriptionDeliveringRestHookSubscriber(Subscription.SubscriptionChannelType theChannelType, BaseSubscriptionInterceptor theSubscriptionInterceptor) { - super(theChannelType, theSubscriptionInterceptor); - } + @Autowired + IResourceRetriever myResourceRetriever; protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription); @@ -105,7 +98,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe } break; case DELETE: - operation = theClient.delete().resourceById(theMsg.getPayloadId(getContext())); + operation = theClient.delete().resourceById(theMsg.getPayloadId(myFhirContext)); break; default: ourLog.warn("Ignoring delivery message of type: {}", theMsg.getOperationType()); @@ -116,7 +109,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe operation.encoded(thePayloadType); } - ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), thePayloadResource.getIdElement().toUnqualified().getValue(), theSubscription.getIdElement(getContext()).toUnqualifiedVersionless().getValue()); + ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), thePayloadResource.getIdElement().toUnqualified().getValue(), theSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue()); try { operation.execute(); @@ -128,16 +121,14 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe } protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) { - IBaseResource payloadResource = theMsg.getPayload(getContext()); + IBaseResource payloadResource = theMsg.getPayload(myFhirContext); if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) { - IIdType payloadId = theMsg.getPayloadId(getContext()); - RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(payloadId.getResourceType()); - IFhirResourceDao dao = getSubscriptionInterceptor().getDao(resourceDef.getImplementingClass()); + IIdType payloadId = theMsg.getPayloadId(myFhirContext); try { - payloadResource = dao.read(payloadId.toVersionless()); + payloadResource = myResourceRetriever.getResource(payloadId.toVersionless()); } catch (ResourceGoneException e) { - ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(getContext())); + ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(myFhirContext)); return null; } } @@ -169,10 +160,10 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe } // Create the client request - getContext().getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + myFhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); IGenericClient client = null; if (isNotBlank(endpointUrl)) { - client = getContext().newRestfulGenericClient(endpointUrl); + client = myFhirContext.newRestfulGenericClient(endpointUrl); // Additional headers specified in the subscription List headers = subscription.getHeaders(); @@ -191,12 +182,11 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe * @param theMsg */ protected void sendNotification(ResourceDeliveryMessage theMsg) { - FhirContext context= getContext(); Map> params = new HashMap(); List
headers = new ArrayList<>(); StringBuilder url = new StringBuilder(theMsg.getSubscription().getEndpointUrl()); - IHttpClient client = context.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers); - IHttpRequest request = client.createParamRequest(context, params, null); + IHttpClient client = myFhirContext.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers); + IHttpRequest request = client.createParamRequest(myFhirContext, params, null); try { IHttpResponse response = request.execute(); } catch (IOException e) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketHandler.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionWebsocketHandler.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketHandler.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionWebsocketHandler.java index 33c541c780e..83ee1fb4c76 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/websocket/SubscriptionWebsocketHandler.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionWebsocketHandler.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.websocket; +package ca.uhn.fhir.jpa.subscription.module.subscriber; /* * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -21,8 +21,8 @@ package ca.uhn.fhir.jpa.subscription.websocket; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; -import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; +import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; @@ -32,18 +32,19 @@ import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; -import java.util.Map; -public class SubscriptionWebsocketHandler extends TextWebSocketHandler implements ISubscriptionWebsocketHandler { +public class SubscriptionWebsocketHandler extends TextWebSocketHandler implements WebSocketHandler { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionWebsocketHandler.class); @Autowired - private SubscriptionWebsocketInterceptor mySubscriptionWebsocketInterceptor; + protected SubscriptionRegistry mySubscriptionRegistry; + @Autowired private FhirContext myCtx; @@ -102,26 +103,24 @@ public class SubscriptionWebsocketHandler extends TextWebSocketHandler implement private class BoundStaticSubscipriptionState implements IState, MessageHandler { - private WebSocketSession mySession; - private CanonicalSubscription mySubscription; + private final WebSocketSession mySession; + private final ActiveSubscription myActiveSubscription; - public BoundStaticSubscipriptionState(WebSocketSession theSession, CanonicalSubscription theSubscription) { + public BoundStaticSubscipriptionState(WebSocketSession theSession, ActiveSubscription theActiveSubscription) { mySession = theSession; - mySubscription = theSubscription; + myActiveSubscription = theActiveSubscription; - String subscriptionId = mySubscription.getIdElement(myCtx).getIdPart(); - mySubscriptionWebsocketInterceptor.registerHandler(subscriptionId, this); + theActiveSubscription.register(this); } @Override public void closing() { - String subscriptionId = mySubscription.getIdElement(myCtx).getIdPart(); - mySubscriptionWebsocketInterceptor.unregisterHandler(subscriptionId, this); + myActiveSubscription.unregister(this); } private void deliver() { try { - String payload = "ping " + mySubscription.getIdElement(myCtx).getIdPart(); + String payload = "ping " + myActiveSubscription.getIdElement(myCtx).getIdPart(); ourLog.info("Sending WebSocket message: {}", payload); mySession.sendMessage(new TextMessage(payload)); } catch (IOException e) { @@ -136,7 +135,7 @@ public class SubscriptionWebsocketHandler extends TextWebSocketHandler implement } try { ResourceDeliveryMessage msg = (ResourceDeliveryMessage) theMessage.getPayload(); - if (mySubscription.equals(msg.getSubscription())) { + if (myActiveSubscription.getSubscription().equals(msg.getSubscription())) { deliver(); } } catch (Exception e) { @@ -177,9 +176,8 @@ public class SubscriptionWebsocketHandler extends TextWebSocketHandler implement } try { - Map idToSubscription = mySubscriptionWebsocketInterceptor.getIdToSubscription(); - CanonicalSubscription subscription = idToSubscription.get(id.getIdPart()); - myState = new BoundStaticSubscipriptionState( theSession, subscription); + ActiveSubscription activeSubscription = mySubscriptionRegistry.get(id.getIdPart()); + myState = new BoundStaticSubscipriptionState( theSession, activeSubscription); } catch (ResourceNotFoundException e) { try { String message = "Invalid bind request - Unknown subscription: " + id.getValue(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/EmailDetails.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/EmailDetails.java similarity index 94% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/EmailDetails.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/EmailDetails.java index 6129ecdc5fc..7fa707b85ae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/EmailDetails.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/EmailDetails.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.email; +package ca.uhn.fhir.jpa.subscription.module.subscriber.email; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/IEmailSender.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/IEmailSender.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/IEmailSender.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/IEmailSender.java index 55b0d7f5a08..4a6df533d1c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/IEmailSender.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/IEmailSender.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.email; +package ca.uhn.fhir.jpa.subscription.module.subscriber.email; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/JavaMailEmailSender.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/JavaMailEmailSender.java index c4c101e5cca..1d0d5be09a5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/JavaMailEmailSender.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/JavaMailEmailSender.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.email; +package ca.uhn.fhir.jpa.subscription.module.subscriber.email; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/SubscriptionDeliveringEmailSubscriber.java similarity index 72% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java rename to hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/SubscriptionDeliveringEmailSubscriber.java index cb659226e97..bf15c92fa57 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/email/SubscriptionDeliveringEmailSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/email/SubscriptionDeliveringEmailSubscriber.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.email; +package ca.uhn.fhir.jpa.subscription.module.subscriber.email; /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Subscription Server * %% * Copyright (C) 2014 - 2018 University Health Network * %% @@ -20,13 +20,14 @@ package ca.uhn.fhir.jpa.subscription.email; * #L% */ -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; -import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; -import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.module.subscriber.BaseSubscriptionDeliverySubscriber; +import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -37,16 +38,17 @@ import static org.apache.commons.lang3.StringUtils.*; @Component @Scope("prototype") - public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliverySubscriber { private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringEmailSubscriber.class); - private SubscriptionEmailInterceptor mySubscriptionEmailInterceptor; + @Autowired + private ModelConfig myModelConfig; - public SubscriptionDeliveringEmailSubscriber(Subscription.SubscriptionChannelType theChannelType, SubscriptionEmailInterceptor theSubscriptionEmailInterceptor) { - super(theChannelType, theSubscriptionEmailInterceptor); + private IEmailSender myEmailSender; - mySubscriptionEmailInterceptor = theSubscriptionEmailInterceptor; + @Autowired + public SubscriptionDeliveringEmailSubscriber(IEmailSender theEmailSender) { + myEmailSender = theEmailSender; } @Override @@ -64,7 +66,7 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv } } - String from = processEmailAddressUri(defaultString(subscription.getEmailDetails().getFrom(), mySubscriptionEmailInterceptor.getDefaultFromAddress())); + String from = processEmailAddressUri(defaultString(subscription.getEmailDetails().getFrom(), myModelConfig.getEmailFromAddress())); String subjectTemplate = defaultString(subscription.getEmailDetails().getSubjectTemplate(), provideDefaultSubjectTemplate()); EmailDetails details = new EmailDetails(); @@ -72,10 +74,9 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv details.setFrom(from); details.setBodyTemplate(subscription.getPayloadString()); details.setSubjectTemplate(subjectTemplate); - details.setSubscription(subscription.getIdElement(getContext())); + details.setSubscription(subscription.getIdElement(myFhirContext)); - IEmailSender emailSender = mySubscriptionEmailInterceptor.getEmailSender(); - emailSender.send(details); + myEmailSender.send(details); } private String processEmailAddressUri(String next) { @@ -86,8 +87,11 @@ public class SubscriptionDeliveringEmailSubscriber extends BaseSubscriptionDeliv return next; } - private String provideDefaultSubjectTemplate() { return "HAPI FHIR Subscriptions"; } + + public void setEmailSender(IEmailSender theEmailSender) { + myEmailSender = theEmailSender; + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java deleted file mode 100644 index b06f3434592..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionDstu3Test.java +++ /dev/null @@ -1,8 +0,0 @@ -package ca.uhn.fhir.jpa.subscription; - -import ca.uhn.fhir.jpa.subscription.config.TestSubscriptionDstu3Config; -import org.springframework.test.context.ContextConfiguration; - -@ContextConfiguration(classes = {TestSubscriptionDstu3Config.class}) -public abstract class BaseSubscriptionDstu3Test extends BaseSubscriptionTest { -} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java deleted file mode 100644 index c62a44c766e..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package ca.uhn.fhir.jpa.subscription; - -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.jpa.subscription.config.MockSearchParamProvider; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -@RunWith(SpringJUnit4ClassRunner.class) -public abstract class BaseSubscriptionTest { - - @Autowired - ISearchParamProvider mySearchParamProvider; - - @Autowired - ISearchParamRegistry mySearchParamRegistry; - - public void setSearchParamBundleResponse(IBundleProvider theBundleProvider) { - ((MockSearchParamProvider)mySearchParamProvider).setBundleProvider(theBundleProvider); - mySearchParamRegistry.forceRefresh(); - } - - -} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java deleted file mode 100644 index be4dc5d128b..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionDstu3Config.java +++ /dev/null @@ -1,25 +0,0 @@ -package ca.uhn.fhir.jpa.subscription.config; - -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu3; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import(TestSubscriptionConfig.class) -public class TestSubscriptionDstu3Config extends BaseSubscriptionDstu3Config { - @Bean - @Override - public ISearchParamProvider searchParamProvider() { - return new MockSearchParamProvider(); - } - - @Bean - @Override - public ISearchParamRegistry searchParamRegistry() { - return new SearchParamRegistryDstu3(searchParamProvider()); - } - -} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionDstu3Test.java new file mode 100644 index 00000000000..8ae2afbc84e --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionDstu3Test.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.jpa.subscription.module; + +import ca.uhn.fhir.jpa.subscription.module.config.TestSubscriptionDstu3Config; +import ca.uhn.fhir.util.StopWatch; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.test.context.ContextConfiguration; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.fail; + +@ContextConfiguration(classes = {TestSubscriptionDstu3Config.class}) +public abstract class BaseSubscriptionDstu3Test extends BaseSubscriptionTest { + public static void waitForSize(int theTarget, List theList) { + StopWatch sw = new StopWatch(); + while (theList.size() != theTarget && sw.getMillis() <= 16000) { + try { + Thread.sleep(50); + } catch (InterruptedException theE) { + throw new Error(theE); + } + } + if (sw.getMillis() >= 16000) { + String describeResults = theList + .stream() + .map(t -> { + if (t == null) { + return "null"; + } + if (t instanceof IBaseResource) { + return ((IBaseResource) t).getIdElement().getValue(); + } + return t.toString(); + }) + .collect(Collectors.joining(", ")); + fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java new file mode 100644 index 00000000000..e81033463ca --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.subscription.module; + +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +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.config.MockFhirClientSearchParamProvider; +import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSubscriptionProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +public abstract class BaseSubscriptionTest { + + @Autowired + ISearchParamProvider mySearchParamProvider; + + @Autowired + ISearchParamRegistry mySearchParamRegistry; + + @Autowired + ISubscriptionProvider mySubscriptionProvider; + + @Autowired + SubscriptionLoader mySubscriptionLoader; + + public void initSearchParamRegistry(IBundleProvider theBundleProvider) { + ((MockFhirClientSearchParamProvider)mySearchParamProvider).setBundleProvider(theBundleProvider); + mySearchParamRegistry.forceRefresh(); + } + + public void initSubscriptionLoader(IBundleProvider theBundleProvider) { + ((MockFhirClientSubscriptionProvider)mySubscriptionProvider).setBundleProvider(theBundleProvider); + mySubscriptionLoader.doInitSubscriptionsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedTest.java new file mode 100644 index 00000000000..d35365db71b --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedTest.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.jpa.subscription.module; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.r4.model.Organization; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ResourceModifiedTest { + private FhirContext myFhirContext = FhirContext.forR4(); + + @Test + public void testCreate() { + Organization org = new Organization(); + org.setName("testOrgName"); + org.setId("testOrgId"); + ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, org, ResourceModifiedMessage.OperationTypeEnum.CREATE); + assertEquals(org.getIdElement(), msg.getId(myFhirContext)); + assertEquals(ResourceModifiedMessage.OperationTypeEnum.CREATE, msg.getOperationType()); + Organization decodedOrg = (Organization) msg.getNewPayload(myFhirContext); + assertEquals(org.getId(), decodedOrg.getId()); + assertEquals(org.getName(), decodedOrg.getName()); + } + + @Test + public void testUpdate() { + Organization org = new Organization(); + org.setName("testOrgName"); + org.setId("testOrgId"); + ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, org, ResourceModifiedMessage.OperationTypeEnum.UPDATE); + assertEquals(org.getIdElement(), msg.getId(myFhirContext)); + assertEquals(ResourceModifiedMessage.OperationTypeEnum.UPDATE, msg.getOperationType()); + Organization decodedOrg = (Organization) msg.getNewPayload(myFhirContext); + assertEquals(org.getId(), decodedOrg.getId()); + assertEquals(org.getName(), decodedOrg.getName()); + } + + @Test + public void testDelete() { + Organization org = new Organization(); + org.setName("testOrgName"); + org.setId("testOrgId"); + ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, org, ResourceModifiedMessage.OperationTypeEnum.DELETE); + assertEquals(org.getIdElement(), msg.getId(myFhirContext)); + assertEquals(ResourceModifiedMessage.OperationTypeEnum.DELETE, msg.getOperationType()); + assertNull(msg.getNewPayload(myFhirContext)); + } + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java similarity index 63% rename from hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java rename to hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java index 071e3c24bce..cbc47ae2e23 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/MockSearchParamProvider.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java @@ -1,14 +1,14 @@ -package ca.uhn.fhir.jpa.subscription.config; +package ca.uhn.fhir.jpa.subscription.module.config; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.jpa.subscription.FhirClientSearchParamProvider; +import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSearchParamProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; -public class MockSearchParamProvider extends FhirClientSearchParamProvider { +public class MockFhirClientSearchParamProvider extends FhirClientSearchParamProvider { private IBundleProvider myBundleProvider = new SimpleBundleProvider(); - public MockSearchParamProvider() { + public MockFhirClientSearchParamProvider() { super(null); } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java new file mode 100644 index 00000000000..efe5326da84 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.jpa.subscription.module.config; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSubscriptionProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; + +public class MockFhirClientSubscriptionProvider extends FhirClientSubscriptionProvider { + private IBundleProvider myBundleProvider = new SimpleBundleProvider(); + + + public MockFhirClientSubscriptionProvider() { + super(null); + } + + public void setBundleProvider(IBundleProvider theBundleProvider) { + myBundleProvider = theBundleProvider; + } + + @Override + public IBundleProvider search(SearchParameterMap theParams) { + return myBundleProvider; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java similarity index 76% rename from hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java rename to hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java index a974094f491..a2f4e2cb4cb 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/config/TestSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java @@ -1,7 +1,8 @@ -package ca.uhn.fhir.jpa.subscription.config; +package ca.uhn.fhir.jpa.subscription.module.config; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.PortUtil; import org.springframework.beans.factory.annotation.Autowired; @@ -28,4 +29,9 @@ public class TestSubscriptionConfig { return myFhirContext.newRestfulGenericClient(ourServerBase); }; + + @Bean + public InMemorySubscriptionMatcher inMemorySubscriptionMatcher() { + return new InMemorySubscriptionMatcher(); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java new file mode 100644 index 00000000000..883fa7c5233 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java @@ -0,0 +1,22 @@ +package ca.uhn.fhir.jpa.subscription.module.config; + +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +@Configuration +@Import(TestSubscriptionConfig.class) +public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { + @Bean + @Primary + public ISearchParamProvider searchParamProvider() { + return new MockFhirClientSearchParamProvider(); + } + + @Bean + @Primary + public ISubscriptionProvider subsriptionProvider() { return new MockFhirClientSubscriptionProvider();} +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java similarity index 96% rename from hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java rename to hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index ca60086a3ab..4b51e775359 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/matcher/SubscriptionMatcherInMemoryTestR3.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java @@ -1,6 +1,6 @@ -package ca.uhn.fhir.jpa.subscription.matcher; +package ca.uhn.fhir.jpa.subscription.module.matcher; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDstu3Test; +import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import org.hl7.fhir.dstu3.model.*; @@ -15,23 +15,23 @@ import java.util.Arrays; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class SubscriptionMatcherInMemoryTestR3 extends BaseSubscriptionDstu3Test { +public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { @Autowired - SubscriptionMatcherInMemory mySubscriptionMatcherInMemory; + InMemorySubscriptionMatcher myInMemorySubscriptionMatcher; private void assertUnsupported(IBaseResource resource, String criteria) { - assertFalse(mySubscriptionMatcherInMemory.match(criteria, resource).supported()); + assertFalse(myInMemorySubscriptionMatcher.match(criteria, resource).supported()); } private void assertMatched(IBaseResource resource, String criteria) { - SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource); + SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, resource); assertTrue(result.supported()); assertTrue(result.matched()); } private void assertNotMatched(IBaseResource resource, String criteria) { - SubscriptionMatchResult result = mySubscriptionMatcherInMemory.match(criteria, resource); + SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, resource); assertTrue(result.supported()); assertFalse(result.matched()); @@ -282,7 +282,7 @@ public class SubscriptionMatcherInMemoryTestR3 extends BaseSubscriptionDstu3Test sp.setStatus(Enumerations.PublicationStatus.ACTIVE); IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); - setSearchParamBundleResponse(bundle); + initSearchParamRegistry(bundle); { Provenance prov = new Provenance(); @@ -314,7 +314,7 @@ public class SubscriptionMatcherInMemoryTestR3 extends BaseSubscriptionDstu3Test sp.setStatus(Enumerations.PublicationStatus.ACTIVE); IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); - setSearchParamBundleResponse(bundle); + initSearchParamRegistry(bundle); { BodySite bodySite = new BodySite(); @@ -406,7 +406,7 @@ public class SubscriptionMatcherInMemoryTestR3 extends BaseSubscriptionDstu3Test sp.setStatus(Enumerations.PublicationStatus.ACTIVE); IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); - setSearchParamBundleResponse(bundle); + initSearchParamRegistry(bundle); { ProcedureRequest pr = new ProcedureRequest(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseSubscriptionChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseSubscriptionChannelDstu3Test.java new file mode 100644 index 00000000000..1c8ba0b36c0 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseSubscriptionChannelDstu3Test.java @@ -0,0 +1,175 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; +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.SubscriptionCheckingSubscriberTest; +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.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.util.PortUtil; +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.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.SubscribableChannel; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class BaseSubscriptionChannelDstu3Test extends BaseSubscriptionDstu3Test { + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriberTest.class); + + @Autowired + FhirContext myFhirContext; + @Autowired + StandaloneSubscriptionMessageHandler myStandaloneSubscriptionMessageHandler; + @Autowired + ISubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; + + private static int ourListenerPort; + private static RestfulServer ourListenerRestServer; + private static Server ourListenerServer; + protected static String ourListenerServerBase; + protected static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); + protected static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); + protected static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); + private static SubscribableChannel ourSubscribableChannel; + private List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); + private long idCounter = 0; + + @After + public void afterUnregisterRestHookListener() { + mySubscriptionIds.clear(); + } + + @Before + public void beforeReset() { + ourCreatedObservations.clear(); + ourUpdatedObservations.clear(); + ourContentTypes.clear(); + if (ourSubscribableChannel == null) { + ourSubscribableChannel = mySubscriptionDeliveryChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); + ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); + } + } + + public T sendResource(T theResource) { + ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); + ResourceModifiedJsonMessage message = new ResourceModifiedJsonMessage(msg); + ourSubscribableChannel.send(message); + return theResource; + } + + protected Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { + Subscription subscription = newSubscription(theCriteria, thePayload, theEndpoint); + + return sendResource(subscription); + } + + protected Subscription newSubscription(String theCriteria, String thePayload, String theEndpoint) { + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); + subscription.setCriteria(theCriteria); + ++idCounter; + IdType id = new IdType("Subscription", idCounter); + subscription.setId(id); + + Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); + channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); + channel.setPayload(thePayload); + channel.setEndpoint(theEndpoint); + subscription.setChannel(channel); + return subscription; + } + + protected Observation sendObservation(String code, String system) { + Observation observation = new Observation(); + ++idCounter; + IdType id = new IdType("Observation", idCounter); + observation.setId(id); + + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code); + coding.setSystem(system); + + observation.setStatus(Observation.ObservationStatus.FINAL); + + return sendResource(observation); + } + + + @BeforeClass + public static void startListenerServer() throws Exception { + ourListenerPort = PortUtil.findFreePort(); + ourListenerRestServer = new RestfulServer(FhirContext.forDstu3()); + ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; + + ObservationListener obsListener = new ObservationListener(); + ourListenerRestServer.setResourceProviders(obsListener); + + ourListenerServer = new Server(ourListenerPort); + + ServletContextHandler proxyHandler = new ServletContextHandler(); + proxyHandler.setContextPath("/"); + + ServletHolder servletHolder = new ServletHolder(); + servletHolder.setServlet(ourListenerRestServer); + proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + + ourListenerServer.setHandler(proxyHandler); + ourListenerServer.start(); + } + + @AfterClass + public static void stopListenerServer() throws Exception { + ourListenerServer.stop(); + } + + public static class ObservationListener implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { + ourLog.info("Received Listener Create"); + ourContentTypes.add(theRequest.getHeader(ca.uhn.fhir.rest.api.Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); + ourCreatedObservations.add(theObservation); + return new MethodOutcome(new IdType("Observation/1"), true); + } + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Update + public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { + ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); + ourUpdatedObservations.add(theObservation); + ourLog.info("Received Listener Update (now have {} updates)", ourUpdatedObservations.size()); + return new MethodOutcome(new IdType("Observation/1"), false); + } + + } + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java new file mode 100644 index 00000000000..d843316de62 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import org.hl7.fhir.dstu3.model.Subscription; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class SubscriptionLoaderFhirClientTest extends BaseSubscriptionChannelDstu3Test { + private String myCode = "1000000050"; + + @Before + public void loadSubscriptions() { + 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 subs = new ArrayList<>(); + subs.add(newSubscription(criteria1, payload, ourListenerServerBase)); + subs.add(newSubscription(criteria2, payload, ourListenerServerBase)); + + IBundleProvider bundle = new SimpleBundleProvider(new ArrayList<>(subs), "uuid"); + initSubscriptionLoader(bundle); + } + + @Test + public void testSubscriptionLoaderFhirClient() throws Exception { + sendObservation(myCode, "SNOMED-CT"); + + waitForSize(0, ourCreatedObservations); + waitForSize(1, ourUpdatedObservations); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java new file mode 100644 index 00000000000..ac8d461455a --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java @@ -0,0 +1,69 @@ +package ca.uhn.fhir.jpa.subscription.module.subscriber; + +import ca.uhn.fhir.jpa.subscription.module.standalone.BaseSubscriptionChannelDstu3Test; +import ca.uhn.fhir.rest.api.Constants; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.assertEquals; + +/** + * Tests copied from jpa.subscription.resthook.RestHookTestDstu3Test + */ +public class SubscriptionCheckingSubscriberTest extends BaseSubscriptionChannelDstu3Test { + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCheckingSubscriberTest.class); + + @Test + public void testRestHookSubscriptionApplicationFhirJson() throws Exception { + String payload = "application/fhir+json"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + + createSubscription(criteria1, payload, ourListenerServerBase); + createSubscription(criteria2, payload, ourListenerServerBase); + + sendObservation(code, "SNOMED-CT"); + + waitForSize(0, ourCreatedObservations); + waitForSize(1, ourUpdatedObservations); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + } + + @Test + public void testRestHookSubscriptionApplicationXmlJson() throws Exception { + String payload = "application/fhir+xml"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + + createSubscription(criteria1, payload, ourListenerServerBase); + createSubscription(criteria2, payload, ourListenerServerBase); + + sendObservation(code, "SNOMED-CT"); + + waitForSize(0, ourCreatedObservations); + waitForSize(1, ourUpdatedObservations); + assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); + } + + @Test + public void testRestHookSubscriptionWithoutPayload() throws Exception { + String payload = ""; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111"; + + createSubscription(criteria1, payload, ourListenerServerBase); + createSubscription(criteria2, payload, ourListenerServerBase); + + sendObservation(code, "SNOMED-CT"); + + waitForSize(0, ourCreatedObservations); + waitForSize(0, ourUpdatedObservations); + } +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index f5d2d3d67ce..76c75078361 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -21,7 +21,6 @@ import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.interceptor.BanUnsupportedHttpMethodsInterceptor; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; -import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhirtest.config.*; import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; @@ -35,7 +34,6 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; -import java.util.Collection; import java.util.List; public class TestRestfulServer extends RestfulServer { @@ -225,15 +223,6 @@ public class TestRestfulServer extends RestfulServer { * Spool results to the database */ setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - - /* - * Load interceptors for the server from Spring - */ - Collection interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values(); - for (IServerInterceptor interceptor : interceptorBeans) { - this.registerInterceptor(interceptor); - } - } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index fd191539ebe..15298ad10f3 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -180,12 +180,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer TEXT_ENCODE_ELEMENTS = new HashSet(Arrays.asList("Bundle", "*.text", "*.(mandatory)")); private static Map myFhirContextMap = Collections.synchronizedMap(new HashMap()); + private enum NarrativeModeEnum { + NORMAL, ONLY, SUPPRESS; + + public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { + return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); + } + } + + /** + * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} + */ + public static class ResponseEncoding { + private final String myContentType; + private final EncodingEnum myEncoding; + private final Boolean myNonLegacy; + + public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { + super(); + myEncoding = theEncoding; + myContentType = theContentType; + if (theContentType != null) { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); + } else { + myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); + } + } else { + FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); + if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { + myNonLegacy = null; + } else { + myNonLegacy = Boolean.TRUE; + } + } + } + + public String getContentType() { + return myContentType; + } + + public EncodingEnum getEncoding() { + return myEncoding; + } + + public String getResourceContentType() { + if (Boolean.TRUE.equals(isNonLegacy())) { + return getEncoding().getResourceContentTypeNonLegacy(); + } + return getEncoding().getResourceContentType(); + } + + Boolean isNonLegacy() { + return myNonLegacy; + } + } + public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { // Pretty print boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails); @@ -272,6 +329,15 @@ public class RestfulServerUtils { * equally, returns thePrefer. */ public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) { + return determineResponseEncodingNoDefault(theReq, thePrefer, null); + } + + /** + * Try to determing the response content type, given the request Accept header and + * _format parameter. If a value is provided to thePreferContents, we'll + * prefer to return that value over the native FHIR value. + */ + public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer, String thePreferContentType) { String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT); if (format != null) { for (String nextFormat : format) { @@ -333,12 +399,12 @@ public class RestfulServerUtils { ResponseEncoding encoding; if (endSpaceIndex == -1) { if (startSpaceIndex == 0) { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken, thePreferContentType); } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex), thePreferContentType); } } else { - encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex)); + encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex), thePreferContentType); String remaining = nextToken.substring(endSpaceIndex + 1); StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); while (qualifierTok.hasMoreTokens()) { @@ -476,13 +542,18 @@ public class RestfulServerUtils { return context; } - private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) { + private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType, String thePreferContentType) { EncodingEnum encoding; if (theStrict) { encoding = EncodingEnum.forContentTypeStrict(theContentType); } else { encoding = EncodingEnum.forContentType(theContentType); } + if (isNotBlank(thePreferContentType)) { + if (thePreferContentType.equals(theContentType)) { + return new ResponseEncoding(theFhirContext, encoding, theContentType); + } + } if (encoding == null) { return null; } @@ -749,23 +820,6 @@ public class RestfulServerUtils { return response.sendWriterResponse(theStatusCode, contentType, charset, writer); } - public static String createEtag(String theVersionId) { - return "W/\"" + theVersionId + '"'; - } - - public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { - String[] retVal = theRequest.getParameters().get(theParamName); - if (retVal == null) { - return null; - } - try { - return Integer.parseInt(retVal[0]); - } catch (NumberFormatException e) { - ourLog.debug("Failed to parse {} value '{}': {}", new Object[] {theParamName, retVal[0], e}); - return null; - } - } - // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) { // String countString = theRequest.getParameter(name); // Integer count = null; @@ -779,61 +833,27 @@ public class RestfulServerUtils { // return count; // } + public static String createEtag(String theVersionId) { + return "W/\"" + theVersionId + '"'; + } + + public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { + String[] retVal = theRequest.getParameters().get(theParamName); + if (retVal == null) { + return null; + } + try { + return Integer.parseInt(retVal[0]); + } catch (NumberFormatException e) { + ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e}); + return null; + } + } + public static void validateResourceListNotNull(List theResourceList) { if (theResourceList == null) { throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed"); } } - private enum NarrativeModeEnum { - NORMAL, ONLY, SUPPRESS; - - public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { - return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); - } - } - - /** - * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} - */ - public static class ResponseEncoding { - private final EncodingEnum myEncoding; - private final Boolean myNonLegacy; - - public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { - super(); - myEncoding = theEncoding; - if (theContentType != null) { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); - } else { - myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); - } - } else { - FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); - if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { - myNonLegacy = null; - } else { - myNonLegacy = Boolean.TRUE; - } - } - } - - public EncodingEnum getEncoding() { - return myEncoding; - } - - public String getResourceContentType() { - if (Boolean.TRUE.equals(isNonLegacy())) { - return getEncoding().getResourceContentTypeNonLegacy(); - } - return getEncoding().getResourceContentType(); - } - - public Boolean isNonLegacy() { - return myNonLegacy; - } - } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java new file mode 100644 index 00000000000..27e70452887 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptor.java @@ -0,0 +1,133 @@ +package ca.uhn.fhir.rest.server.interceptor; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * 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.rest.api.Constants; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +/** + * This interceptor allows a client to request that a Media resource be + * served as the raw contents of the resource, assuming either: + *
    + *
  • The client explicitly requests the correct content type using the Accept header
  • + *
  • The client explicitly requests raw output by adding the parameter _output=data
  • + *
+ */ +public class ServeMediaResourceRawInterceptor extends InterceptorAdapter { + + public static final String MEDIA_CONTENT_CONTENT_TYPE_OPT = "Media.content.contentType"; + + private static final Set RESPOND_TO_OPERATION_TYPES; + + static { + Set respondToOperationTypes = new HashSet<>(); + respondToOperationTypes.add(RestOperationTypeEnum.READ); + respondToOperationTypes.add(RestOperationTypeEnum.VREAD); + RESPOND_TO_OPERATION_TYPES = Collections.unmodifiableSet(respondToOperationTypes); + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + if (theResponseObject == null) { + return true; + } + + + FhirContext context = theRequestDetails.getFhirContext(); + String resourceName = context.getResourceDefinition(theResponseObject).getName(); + + // Are we serving a FHIR read request on the Media resource type + if (!"Media".equals(resourceName) || !RESPOND_TO_OPERATION_TYPES.contains(theRequestDetails.getRestOperationType())) { + return true; + } + + // What is the content type of the Media resource we're returning? + String contentType = null; + Optional contentTypeOpt = context.newFluentPath().evaluateFirst(theResponseObject, MEDIA_CONTENT_CONTENT_TYPE_OPT, IPrimitiveType.class); + if (contentTypeOpt.isPresent()) { + contentType = contentTypeOpt.get().getValueAsString(); + } + + // What is the data of the Media resource we're returning? + IPrimitiveType data = null; + Optional dataOpt = context.newFluentPath().evaluateFirst(theResponseObject, "Media.content.data", IPrimitiveType.class); + if (dataOpt.isPresent()) { + data = dataOpt.get(); + } + + if (isBlank(contentType) || data == null) { + return true; + } + + RestfulServerUtils.ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, null, contentType); + if (responseEncoding != null) { + if (contentType.equals(responseEncoding.getContentType())) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + + } + } + + String[] outputParam = theRequestDetails.getParameters().get("_output"); + if (outputParam != null && "data".equals(outputParam[0])) { + returnRawResponse(theRequestDetails, theServletResponse, contentType, data); + return false; + } + + return true; + } + + private void returnRawResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, String theContentType, IPrimitiveType theData) { + theServletResponse.setStatus(200); + if (theRequestDetails.getServer() instanceof RestfulServer) { + RestfulServer rs = (RestfulServer) theRequestDetails.getServer(); + rs.addHeadersToResponse(theServletResponse); + } + + theServletResponse.addHeader(Constants.HEADER_CONTENT_TYPE, theContentType); + + // Write the response + try { + theServletResponse.getOutputStream().write(theData.getValue()); + theServletResponse.getOutputStream().close(); + } catch (IOException e) { + throw new InternalErrorException(e); + } + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 66044200232..d5a81fb6e91 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -84,6 +84,8 @@ public abstract class BaseMethodBinding { } } + // This allows us to invoke methods on private classes + myMethod.setAccessible(true); } protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List> thePreferTypes) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 19b8a282812..82cf98d41a5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -19,7 +19,6 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.UrlUtil; @@ -57,27 +56,10 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { - protected static final Set ALLOWED_PARAMS; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); - static { - HashSet set = new HashSet(); - set.add(Constants.PARAM_FORMAT); - set.add(Constants.PARAM_NARRATIVE); - set.add(Constants.PARAM_PRETTY); - set.add(Constants.PARAM_SORT); - set.add(Constants.PARAM_SORT_ASC); - set.add(Constants.PARAM_SORT_DESC); - set.add(Constants.PARAM_COUNT); - set.add(Constants.PARAM_SUMMARY); - set.add(Constants.PARAM_ELEMENTS); - set.add(ResponseHighlighterInterceptor.PARAM_RAW); - ALLOWED_PARAMS = Collections.unmodifiableSet(set); - } - private MethodReturnTypeEnum myMethodReturnType; private String myResourceName; - private Class myResourceType; @SuppressWarnings("unchecked") public BaseResourceReturningMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { @@ -112,11 +94,12 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi if (theReturnResourceType != null) { if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { - if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) { - // If we're returning an abstract type, that's ok - } else { - myResourceType = (Class) theReturnResourceType; - myResourceName = theContext.getResourceDefinition(myResourceType).getName(); + + // If we're returning an abstract type, that's ok, but if we know the resource + // type let's grab it + if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) { + Class resourceType = (Class) theReturnResourceType; + myResourceName = theContext.getResourceDefinition(resourceType).getName(); } } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java index 9d3eb9229b8..621034055c9 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ReadMethodBinding.java @@ -23,25 +23,35 @@ package ca.uhn.fhir.rest.server.method; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; -import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.*; -import ca.uhn.fhir.rest.api.server.*; +import ca.uhn.fhir.rest.annotation.Elements; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.IRestfulServer; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.ETagSupportEnum; -import ca.uhn.fhir.rest.server.exceptions.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.util.DateUtils; import javax.annotation.Nonnull; @@ -110,7 +120,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding { return false; } for (String next : theRequest.getParameters().keySet()) { - if (!ALLOWED_PARAMS.contains(next)) { + if (!next.startsWith("_")) { return false; } } @@ -125,8 +135,8 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding { if (isNotBlank(theRequest.getCompartmentName())) { return false; } - if (theRequest.getRequestType() != RequestTypeEnum.GET) { - ourLog.trace("Method {} doesn't match because request type is not GET: {}", theRequest.getId(), theRequest.getRequestType()); + if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.HEAD ) { + ourLog.trace("Method {} doesn't match because request type is not GET or HEAD: {}", theRequest.getId(), theRequest.getRequestType()); return false; } if (Constants.PARAM_HISTORY.equals(theRequest.getOperation())) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index 3b06a9b2f19..ae26f2c1927 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -23,12 +23,10 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.ConfigurationException; @@ -52,12 +50,20 @@ import javax.annotation.Nonnull; public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); + private static final Set SPECIAL_SEARCH_PARAMS; private String myCompartmentName; private String myDescription; private Integer myIdParamIndex; private String myQueryName; private boolean myAllowUnknownParams; + static { + HashSet specialSearchParams = new HashSet<>(); + specialSearchParams.add(IAnyResource.SP_RES_ID); + specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE); + SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams); + } + public SearchMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { super(theReturnResourceType, theMethod, theContext, theProvider); Search search = theMethod.getAnnotation(Search.class); @@ -75,27 +81,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } - /* - * Check for parameter combinations and names that are invalid - */ - List parameters = getParameters(); - for (int i = 0; i < parameters.size(); i++) { - IParameter next = parameters.get(i); - if (!(next instanceof SearchParameter)) { - continue; - } - - SearchParameter sp = (SearchParameter) next; - if (sp.getName().startsWith("_")) { - if (ALLOWED_PARAMS.contains(sp.getName())) { - String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), - sp.getName()); - throw new ConfigurationException(msg); - } - } - - } - /* * Only compartment searching methods may have an ID parameter */ @@ -232,7 +217,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } } for (String next : theRequest.getParameters().keySet()) { - if (ALLOWED_PARAMS.contains(next)) { + if (next.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(next)) { methodParamsTemp.add(next); } } diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java index cf8d2d42f3a..8844fa69321 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.spring.boot.autoconfigure; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; +import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.config.BaseJavaConfigR4; @@ -30,6 +31,7 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.provider.BaseJpaProvider; import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider; +import ca.uhn.fhir.model.dstu2.resource.AuditEvent; import ca.uhn.fhir.okhttp.client.OkHttpRestfulClientFactory; import ca.uhn.fhir.rest.client.apache.ApacheRestfulClientFactory; import ca.uhn.fhir.rest.client.api.IClientInterceptor; @@ -45,6 +47,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; import okhttp3.OkHttpClient; import org.apache.http.client.HttpClient; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.*; @@ -68,6 +71,7 @@ import org.springframework.util.CollectionUtils; import javax.servlet.ServletException; import javax.sql.DataSource; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; /** * {@link EnableAutoConfiguration Auto-configuration} for HAPI FHIR. @@ -163,26 +167,11 @@ public class FhirAutoConfiguration { @ConditionalOnBean(DataSource.class) @EnableConfigurationProperties(FhirProperties.class) static class FhirJpaServerConfiguration { - - @Bean - @ConditionalOnMissingBean - public ScheduledExecutorFactoryBean scheduledExecutorService() { - ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean(); - b.setPoolSize(5); - return b; - } - - @Bean(name="hapiJpaTaskExecutor") - public AsyncTaskExecutor taskScheduler() { - ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler(); - retVal.setConcurrentExecutor(scheduledExecutorService().getObject()); - retVal.setScheduledExecutor(scheduledExecutorService().getObject()); - return retVal; - } + @Autowired + private ScheduledExecutorService myScheduledExecutorService; @Configuration @EntityScan(basePackages = {"ca.uhn.fhir.jpa.entity", "ca.uhn.fhir.jpa.model.entity"}) - @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") static class FhirJpaDaoConfiguration { @Bean diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java index 72c7df521d0..1627726556c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/test/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfigurationTest.java @@ -1,8 +1,5 @@ package ca.uhn.fhir.spring.boot.autoconfigure; -import java.net.URL; -import java.net.URLClassLoader; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.dao.DaoConfig; @@ -14,18 +11,19 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.spring.boot.autoconfigure.FhirAutoConfiguration.FhirJpaServerConfiguration.Dstu3; import org.assertj.core.util.Arrays; import org.junit.After; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.mock.env.MockEnvironment; + +import java.net.URL; +import java.net.URLClassLoader; import static org.assertj.core.api.Assertions.assertThat; @@ -36,149 +34,157 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class FhirAutoConfigurationTest { - @Rule - public final ExpectedException thrown = ExpectedException.none(); + @Rule + public final ExpectedException thrown = ExpectedException.none(); - private AnnotationConfigApplicationContext context; + private AnnotationConfigApplicationContext context; - @After - public void close() { - if (this.context != null) { - this.context.close(); - } - } + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } - @Test - public void withFhirContext() throws Exception { - load(); - assertThat(this.context.getBeansOfType(FhirContext.class)).hasSize(1); - } + @Test + public void withFhirContext() throws Exception { + load(); + assertThat(this.context.getBeansOfType(FhirContext.class)).hasSize(1); + } - @Test - public void withFhirVersion() throws Exception { - load(Arrays.array(EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class, - FhirAutoConfiguration.class), - "hapi.fhir.version:DSTU3", "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", - "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); - assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.DSTU3.getVersionImplementation()); + @Test + public void withFhirVersion() throws Exception { + load(Arrays.array(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, + FhirAutoConfiguration.class), + "hapi.fhir.version:DSTU3", "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", + "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); + assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.DSTU3.getVersionImplementation()); - load(Arrays.array(EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class, - FhirAutoConfiguration.class), - "hapi.fhir.version:R4", - "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", - "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); - assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.R4.getVersionImplementation()); - } + load(Arrays.array(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, + FhirAutoConfiguration.class), + "hapi.fhir.version:R4", + "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", + "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); + assertThat(this.context.getBean(FhirContext.class).getVersion()).isEqualTo(FhirVersionEnum.R4.getVersionImplementation()); + } - @Test - public void withRestfulServer() { - load("hapi.fhir.server.path:/hapi-fhir/*"); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); - assertThat(this.context.getBeansOfType(RestfulServer.class)).hasSize(1); - assertThat(this.context.getBean(ServletRegistrationBean.class).getUrlMappings()).contains("/hapi-fhir/*"); - } + @Test + public void withRestfulServer() { + load("hapi.fhir.server.path:/hapi-fhir/*"); + assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); + assertThat(this.context.getBeansOfType(RestfulServer.class)).hasSize(1); + assertThat(this.context.getBean(ServletRegistrationBean.class).getUrlMappings()).contains("/hapi-fhir/*"); + } - @Test - public void withJpaServer() { - load( - Arrays.array( - EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, - FhirAutoConfiguration.class), - "hapi.fhir.version:DSTU3", - "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", - "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); - assertThat(this.context.getBeansOfType(DaoConfig.class)).hasSize(1); - assertThat(this.context.getBeansOfType(Dstu3.class)).hasSize(1); - } + @Test + public void withJpaServer() { + load( + Arrays.array( + EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + FhirAutoConfiguration.class), + "hapi.fhir.version:DSTU3", + "spring.jpa.properties.hibernate.search.default.indexBase:target/lucenefiles", + "spring.jpa.properties.hibernate.search.model_mapping:ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory"); + assertThat(this.context.getBeansOfType(DaoConfig.class)).hasSize(1); + assertThat(this.context.getBeansOfType(Dstu3.class)).hasSize(1); + } - @Test - public void withNoValidation() { - load("hapi.fhir.validation.enabled:false"); - this.thrown.expect(NoSuchBeanDefinitionException.class); - this.context.getBean(RequestValidatingInterceptor.class); - } + @Test + public void withNoValidation() { + load("hapi.fhir.validation.enabled:false"); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(RequestValidatingInterceptor.class); + } - @Test - public void withValidation() { - load(); - assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(1); - } + @Test + public void withValidation() { + load(); + assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(1); + } - @Test - public void withValidations() { - load("hapi.fhir.validation.request-only=false"); - assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(2); - } + @Test + public void withValidations() { + load("hapi.fhir.validation.request-only:false"); + assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(2); + } - @Test - public void withCustomValidationSchemaLocation() { - load("hapi.fhir.validation.schema-location:custom-schema-location"); - assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(1); - } + @Test + public void withCustomValidationSchemaLocation() { + load("hapi.fhir.validation.schema-location:custom-schema-location"); + assertThat(this.context.getBeansOfType(IServerInterceptor.class)).hasSize(1); + } - @Test - public void withApacheHttpClient() { - load(new HidePackagesClassLoader("okhttp3"), "hapi.fhir.server.url:http://localhost:8080"); - assertThat(this.context.getBeansOfType(ApacheRestfulClientFactory.class)).hasSize(1); - assertThat(this.context.getBeansOfType(OkHttpRestfulClientFactory.class)).hasSize(0); - } + @Test + public void withApacheHttpClient() { + load(new HidePackagesClassLoader("okhttp3"), "hapi.fhir.server.url:http://localhost:8080"); + assertThat(this.context.getBeansOfType(ApacheRestfulClientFactory.class)).hasSize(1); + assertThat(this.context.getBeansOfType(OkHttpRestfulClientFactory.class)).hasSize(0); + } - @Test - public void withOkHttpClient() { - load("hapi.fhir.server.url:http://localhost:8080"); - assertThat(this.context.getBeansOfType(OkHttpRestfulClientFactory.class)).hasSize(1); - assertThat(this.context.getBeansOfType(ApacheRestfulClientFactory.class)).hasSize(0); - } + @Test + public void withOkHttpClient() { + load("hapi.fhir.server.url:http://localhost:8080"); + assertThat(this.context.getBeansOfType(OkHttpRestfulClientFactory.class)).hasSize(1); + assertThat(this.context.getBeansOfType(ApacheRestfulClientFactory.class)).hasSize(0); + } - private void load(String... environment) { - load(new Class[] { FhirAutoConfiguration.class }, null, environment); - } + private void load(String... environment) { + load(new Class[]{FhirAutoConfiguration.class}, null, environment); + } - private void load(ClassLoader classLoader, String... environment) { - load(new Class[] { FhirAutoConfiguration.class }, classLoader, environment); - } + private void load(ClassLoader classLoader, String... environment) { + load(new Class[]{FhirAutoConfiguration.class}, classLoader, environment); + } - private void load(Class[] configs, String... environment) { - load(configs, null, environment); - } + private void load(Class[] configs, String... environment) { + load(configs, null, environment); + } - private void load(Class[] configs, ClassLoader classLoader, String... environment) { - AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); - EnvironmentTestUtils.addEnvironment(applicationContext, environment); - if (classLoader != null) { - applicationContext.setClassLoader(classLoader); - } - if (configs != null) { - applicationContext.register(configs); - } - applicationContext.refresh(); - this.context = applicationContext; - } + private void load(Class[] configs, ClassLoader classLoader, String... environment) { - private static final class HidePackagesClassLoader extends URLClassLoader { + MockEnvironment env = new MockEnvironment(); + for (String next : environment) { + String nextKey = next.substring(0, next.indexOf(':')); + String nextValue = next.substring(next.indexOf(':') + 1); + env.setProperty(nextKey, nextValue); + } - private final String[] hiddenPackages; + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.setEnvironment(env); + if (classLoader != null) { + applicationContext.setClassLoader(classLoader); + } + if (configs != null) { + applicationContext.register(configs); + } + applicationContext.refresh(); + this.context = applicationContext; + } - private HidePackagesClassLoader(String... hiddenPackages) { - super(new URL[0], FhirAutoConfigurationTest.class.getClassLoader()); - this.hiddenPackages = hiddenPackages; - } + private static final class HidePackagesClassLoader extends URLClassLoader { - @Override - protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - for (String hiddenPackage : this.hiddenPackages) { - if (name.startsWith(hiddenPackage)) { - throw new ClassNotFoundException(); - } - } - return super.loadClass(name, resolve); - } + private final String[] hiddenPackages; - } + private HidePackagesClassLoader(String... hiddenPackages) { + super(new URL[0], FhirAutoConfigurationTest.class.getClassLoader()); + this.hiddenPackages = hiddenPackages; + } + + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + for (String hiddenPackage : this.hiddenPackages) { + if (name.startsWith(hiddenPackage)) { + throw new ClassNotFoundException(); + } + } + return super.loadClass(name, resolve); + } + + } } diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java index a94bb83fea0..76060a6dc17 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/src/test/java/sample/fhir/server/jpa/SampleJpaRestfulServerApplicationTest.java @@ -4,14 +4,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.IGenericClient; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 5e0d5a2c16f..cba39f3228a 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -214,15 +214,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 26ff214acf0..99d2ba65c71 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -119,15 +119,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime @@ -207,18 +211,18 @@ org.jacoco jacoco-maven-plugin - - ${basedir}/target/classes - ${basedir}/../hapi-fhir-base/target/classes - ${basedir}/../hapi-fhir-client/target/classes - ${basedir}/../hapi-fhir-server/target/classes - - - ${basedir}/src/main/java - ${basedir}/../hapi-fhir-base/src/main/java - ${basedir}/../hapi-fhir-client/src/main/java - ${basedir}/../hapi-fhir-server/src/main/java - + + + + + + + + + + + + true @@ -356,7 +360,8 @@ ca.uhn.fhir.model.dstu2 dstu2 true - + ca.uhn.fhir.model.dstu2 + diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java index 1194bceca00..cba11dc0a9a 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java @@ -2085,6 +2085,19 @@ public class JsonParserDstu2Test { Assert.assertThat(message, containsString("contained")); } + + @Test + public void testParseQuestionnaireResponseAnswerWithValueReference() { + String response = "{\"resourceType\":\"QuestionnaireResponse\",\"group\":{\"question\":[{\"answer\": [{\"valueReference\": {\"reference\": \"Observation/testid\"}}]}]}}"; + QuestionnaireResponse r = ourCtx.newJsonParser().parseResource(QuestionnaireResponse.class, response); + + QuestionnaireResponse.GroupQuestionAnswer answer = r.getGroup().getQuestion().get(0).getAnswer().get(0); + assertNotNull(answer); + assertNotNull(answer.getValue()); + assertEquals("Observation/testid", ((ResourceReferenceDt)answer.getValue()).getReference().getValue()); + } + + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java index fa3c5d8bd4d..a9cc0796324 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesDstu2Test.java @@ -132,7 +132,7 @@ public class ServerFeaturesDstu2Test { ourLog.info(status.toString()); - assertEquals(400, status.getStatusLine().getStatusCode()); + assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(status.getFirstHeader("x-powered-by").getValue(), containsString("HAPI")); } diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 9d6604bca4c..7e4c703fbf7 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -245,15 +245,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java index ee8baaccde0..11da788d945 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FluentPathDstu3.java @@ -11,6 +11,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import java.util.List; +import java.util.Optional; public class FluentPathDstu3 implements IFluentPath { @@ -43,4 +44,9 @@ public class FluentPathDstu3 implements IFluentPath { return (List) result; } + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } + } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptorDstu3Test.java index b6edd6971c5..9e6a300ea69 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupprtedHttpMethodsInterceptorDstu3Test.java @@ -54,19 +54,28 @@ public class BanUnsupprtedHttpMethodsInterceptorDstu3Test { IOUtils.closeQuietly(status.getEntity().getContent()); } } - - @Test - public void testHeadJson() throws Exception { - HttpHead httpGet = new HttpHead("http://localhost:" + ourPort + "/Patient/123"); - HttpResponse status = ourClient.execute(httpGet); - assertEquals(null, status.getEntity()); - - ourLog.info(status.toString()); - - assertEquals(400, status.getStatusLine().getStatusCode()); - assertThat(status.getFirstHeader("x-powered-by").getValue(), containsString("HAPI")); + + @Test + public void testHeadJsonWithInvalidPatient() throws Exception { + HttpHead httpGet = new HttpHead("http://localhost:" + ourPort + "/Patient/123"); + HttpResponse status = ourClient.execute(httpGet); + assertEquals(null, status.getEntity()); + ourLog.info(status.toString()); + + assertEquals(404, status.getStatusLine().getStatusCode()); + assertThat(status.getFirstHeader("x-powered-by").getValue(), containsString("HAPI")); + } + + @Test + public void testHeadJsonWithValidPatient() throws Exception { + HttpHead httpGet = new HttpHead("http://localhost:" + ourPort + "/Patient/1"); + HttpResponse status = ourClient.execute(httpGet); + assertEquals(null, status.getEntity()); + ourLog.info(status.toString()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + assertThat(status.getFirstHeader("x-powered-by").getValue(), containsString("HAPI")); } - @Test public void testHttpTrackNotEnabled() throws Exception { diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 296ee6048c5..d2c626d7014 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -143,15 +143,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/QuestionnaireResponse.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/QuestionnaireResponse.java index d55266f07ed..bb8efa25165 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/QuestionnaireResponse.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/instance/model/QuestionnaireResponse.java @@ -831,7 +831,7 @@ public class QuestionnaireResponse extends DomainResource { /** * The answer (or one of the answers) provided by the respondent to the question. */ - @Child(name = "value", type = {BooleanType.class, DecimalType.class, IntegerType.class, DateType.class, DateTimeType.class, InstantType.class, TimeType.class, StringType.class, UriType.class, Attachment.class, Coding.class, Quantity.class}, order=1, min=0, max=1, modifier=false, summary=false) + @Child(name = "value", type = {BooleanType.class, DecimalType.class, IntegerType.class, DateType.class, DateTimeType.class, InstantType.class, TimeType.class, StringType.class, UriType.class, Attachment.class, Coding.class, Quantity.class, Reference.class}, order=1, min=0, max=1, modifier=false, summary=false) @Description(shortDefinition="Single-valued answer to the question", formalDefinition="The answer (or one of the answers) provided by the respondent to the question." ) protected Type value; diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java index 967273f9861..0cea7e4c231 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java @@ -11,6 +11,7 @@ import org.apache.commons.io.IOUtils; import org.hamcrest.core.IsNot; import org.hamcrest.core.StringContains; import org.hamcrest.text.StringContainsInOrder; +import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.*; import org.hl7.fhir.instance.model.Address.AddressUse; import org.hl7.fhir.instance.model.Address.AddressUseEnumFactory; @@ -19,6 +20,7 @@ import org.hl7.fhir.instance.model.Conformance.UnknownContentCode; import org.hl7.fhir.instance.model.Identifier.IdentifierUse; import org.hl7.fhir.instance.model.Narrative.NarrativeStatus; import org.hl7.fhir.instance.model.Patient.ContactComponent; +import org.hl7.fhir.instance.model.QuestionnaireResponse.QuestionAnswerComponent; import org.hl7.fhir.instance.model.ValueSet.ConceptDefinitionComponent; import org.hl7.fhir.instance.model.ValueSet.ValueSetCodeSystemComponent; import org.hl7.fhir.instance.model.api.IIdType; @@ -1257,6 +1259,17 @@ public class JsonParserHl7OrgDstu2Test { Assert.assertEquals(refVal, ((Reference) extlst.get(0).getValue()).getReference()); } + @Test + public void testParseQuestionnaireResponseAnswerWithValueReference() throws FHIRException { + String response = "{\"resourceType\":\"QuestionnaireResponse\",\"group\":{\"question\":[{\"answer\": [{\"valueReference\": {\"reference\": \"Observation/testid\"}}]}]}}"; + QuestionnaireResponse r = ourCtx.newJsonParser().parseResource(QuestionnaireResponse.class, response); + + QuestionAnswerComponent answer = r.getGroup().getQuestion().get(0).getAnswer().get(0); + assertNotNull(answer); + assertNotNull(answer.getValueReference()); + assertEquals("Observation/testid", answer.getValueReference().getReference()); + } + @ResourceDef(name = "Patient") public static class MyPatientWithOneDeclaredAddressExtension extends Patient { diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 47f0aa435ee..1d6909efc5c 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -153,15 +153,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java index ae5dcbba6a7..45e9aa7e27f 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FluentPathR4.java @@ -1,7 +1,8 @@ package org.hl7.fhir.r4.hapi.fluentpath; -import java.util.List; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fluentpath.FluentPathExecutionException; +import ca.uhn.fhir.fluentpath.IFluentPath; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; @@ -9,39 +10,44 @@ import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.utils.FHIRPathEngine; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.fluentpath.FluentPathExecutionException; -import ca.uhn.fhir.fluentpath.IFluentPath; +import java.util.List; +import java.util.Optional; -public class FluentPathR4 implements IFluentPath { +public class FluentPathR4 implements IFluentPath { - private FHIRPathEngine myEngine; + private FHIRPathEngine myEngine; - public FluentPathR4(FhirContext theCtx) { - if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { - throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); - } - IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); - myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); - } + public FluentPathR4(FhirContext theCtx) { + if (!(theCtx.getValidationSupport() instanceof IValidationSupport)) { + throw new IllegalStateException("Validation support module configured on context appears to be for the wrong FHIR version- Does not extend " + IValidationSupport.class.getName()); + } + IValidationSupport validationSupport = (IValidationSupport) theCtx.getValidationSupport(); + myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); + } - @SuppressWarnings("unchecked") - @Override - public List evaluate(IBase theInput, String thePath, Class theReturnType) { - List result; - try { - result = myEngine.evaluate((Base)theInput, thePath); - } catch (FHIRException e) { - throw new FluentPathExecutionException(e); - } + @SuppressWarnings("unchecked") + @Override + public List evaluate(IBase theInput, String thePath, Class theReturnType) { + List result; + try { + result = myEngine.evaluate((Base) theInput, thePath); + } catch (FHIRException e) { + throw new FluentPathExecutionException(e); + } + + for (Base next : result) { + if (!theReturnType.isAssignableFrom(next.getClass())) { + throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); + } + } + + return (List) result; + } + + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } - for (Base next : result) { - if (!theReturnType.isAssignableFrom(next.getClass())) { - throw new FluentPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); - } - } - - return (List) result; - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java index 7d2eb8367c6..0a077b6953f 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/XmlParserR4Test.java @@ -195,13 +195,6 @@ public class XmlParserR4Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(o); ourLog.info(encoded); - FhirContext - .forR4() - .newRestfulGenericClient("http://hapi.fhir.org/baseR4") - .create() - .resource(o) - .execute(); - assertThat(encoded, containsString("")); o = ourCtx.newXmlParser().parseResource(Observation.class, encoded); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java new file mode 100644 index 00000000000..469ead376ed --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ServeMediaResourceRawInterceptorTest.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.rest.server.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Media; +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.*; + +public class ServeMediaResourceRawInterceptorTest { + + + private static final Logger ourLog = LoggerFactory.getLogger(ServeMediaResourceRawInterceptorTest.class); + private static int ourPort; + private static RestfulServer ourServlet; + private static FhirContext ourCtx = FhirContext.forR4(); + private static CloseableHttpClient ourClient; + private static Media ourNextResponse; + private static String ourReadUrl; + private ServeMediaResourceRawInterceptor myInterceptor; + + @Before + public void before() { + myInterceptor = new ServeMediaResourceRawInterceptor(); + ourServlet.registerInterceptor(myInterceptor); + } + + @After + public void after() { + ourNextResponse = null; + ourServlet.unregisterInterceptor(myInterceptor); + } + + @Test + public void testMediaHasImageRequestHasNoAcceptHeader() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue()); + String contents = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + assertThat(contents, containsString("\"resourceType\"")); + } + } + + @Test + public void testMediaHasImageRequestHasMatchingAcceptHeader() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + get.addHeader(Constants.HEADER_ACCEPT, "image/png"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("image/png", response.getEntity().getContentType().getValue()); + byte[] contents = IOUtils.toByteArray(response.getEntity().getContent()); + assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents); + } + } + + @Test + public void testMediaHasNoContentType() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl); + get.addHeader(Constants.HEADER_ACCEPT, "image/png"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("application/fhir+json;charset=utf-8", response.getEntity().getContentType().getValue()); + } + } + + @Test + public void testMediaHasImageRequestHasNonMatchingAcceptHeaderOutputRaw() throws IOException { + ourNextResponse = new Media(); + ourNextResponse.getContent().setContentType("image/png"); + ourNextResponse.getContent().setData(new byte[]{2, 3, 4, 5, 6, 7, 8}); + + HttpGet get = new HttpGet(ourReadUrl + "?_output=data"); + try (CloseableHttpResponse response = ourClient.execute(get)) { + assertEquals("image/png", response.getEntity().getContentType().getValue()); + byte[] contents = IOUtils.toByteArray(response.getEntity().getContent()); + assertArrayEquals(new byte[]{2, 3, 4, 5, 6, 7, 8}, contents); + } + } + + private static class MyMediaResourceProvider implements IResourceProvider { + + + @Override + public Class getResourceType() { + return Media.class; + } + + @Read + public Media read(@IdParam IIdType theId) { + return ourNextResponse; + } + + } + + @AfterClass + public static void afterClassClearContext() throws IOException { + ourClient.close(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + + // Create server + ourLog.info("Using port: {}", ourPort); + Server ourServer = new Server(ourPort); + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setDefaultResponseEncoding(EncodingEnum.JSON); + ourServlet.setResourceProviders(new MyMediaResourceProvider()); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + // Create client + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + ourReadUrl = "http://localhost:" + ourPort + "/Media/123"; + } + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java index 6a7cd0120e0..121c5a55abd 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.util.TestUtil; 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.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Observation; @@ -237,7 +238,7 @@ public class HashMapResourceProviderTest { Bundle resp = ourClient .search() .forResource("Patient") - .where(Patient.RES_ID.exactly().codes("2", "3")) + .where(IAnyResource.RES_ID.exactly().codes("2", "3")) .returnBundle(Bundle.class).execute(); assertEquals(2, resp.getTotal()); assertEquals(2, resp.getEntry().size()); @@ -248,8 +249,8 @@ public class HashMapResourceProviderTest { resp = ourClient .search() .forResource("Patient") - .where(Patient.RES_ID.exactly().codes("2", "3")) - .where(Patient.RES_ID.exactly().codes("2", "3")) + .where(IAnyResource.RES_ID.exactly().codes("2", "3")) + .where(IAnyResource.RES_ID.exactly().codes("2", "3")) .returnBundle(Bundle.class).execute(); assertEquals(2, resp.getTotal()); assertEquals(2, resp.getEntry().size()); @@ -259,8 +260,8 @@ public class HashMapResourceProviderTest { resp = ourClient .search() .forResource("Patient") - .where(Patient.RES_ID.exactly().codes("2", "3")) - .where(Patient.RES_ID.exactly().codes("4", "3")) + .where(IAnyResource.RES_ID.exactly().codes("2", "3")) + .where(IAnyResource.RES_ID.exactly().codes("4", "3")) .returnBundle(Bundle.class).execute(); respIds = resp.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); assertThat(respIds, containsInAnyOrder("Patient/3")); diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 8d41fa54e58..0f70b0ba64e 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -75,6 +75,11 @@ provided + + javax.annotation + javax.annotation-api + + ch.qos.logback logback-classic diff --git a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/OverlayTestApp.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/OverlayTestApp.java index 6f17ade358f..1e677cc9e39 100644 --- a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/OverlayTestApp.java +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/OverlayTestApp.java @@ -56,7 +56,7 @@ public class OverlayTestApp { // restServerDstu2.setPagingProvider(new FifoMemoryPagingProvider(10)); // restServerDstu2.setImplementationDescription("This is a great server!!!!"); // restServerDstu2.setFhirContext(ourAppCtx.getBean("myFhirContextDstu2", FhirContext.class)); -// List rpsDev = (List) ourAppCtx.getBean("myResourceProvidersDstu2", List.class); +// List rpsDev = (List) ourAppCtx.getBean("myResourceProvidersDstu2", List.class); // restServerDstu2.setResourceProviders(rpsDev); // // JpaSystemProviderDstu2 systemProvDev = (JpaSystemProviderDstu2) ourAppCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class); @@ -74,7 +74,7 @@ public class OverlayTestApp { // restServerDstu1.setPagingProvider(new FifoMemoryPagingProvider(10)); // restServerDstu1.setImplementationDescription("This is a great server!!!!"); // restServerDstu1.setFhirContext(ourAppCtx.getBean("myFhirContextDstu1", FhirContext.class)); -// List rpsDstu1 = (List) ourAppCtx.getBean("myResourceProvidersDstu1", List.class); +// List rpsDstu1 = (List) ourAppCtx.getBean("myResourceProvidersDstu1", List.class); // restServerDstu1.setResourceProviders(rpsDstu1); // // JpaSystemProviderDstu1 systemProvDstu1 = (JpaSystemProviderDstu1) ourAppCtx.getBean("mySystemProviderDstu1", JpaSystemProviderDstu1.class); diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 6723bc84d70..8d72b9ebeea 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -189,15 +189,19 @@ jaxb-api test + + + + + + + + + + - com.sun.xml.bind - jaxb-core - test - - - com.sun.xml.bind - jaxb-impl - test + org.glassfish.jaxb + jaxb-runtime diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java index 83a227a6541..4ff765a4855 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java @@ -60,6 +60,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid private IValidationSupport myValidationSupport; private boolean noTerminologyChecks = false; private volatile WorkerContextWrapper myWrappedWorkerContext; + private List extensionDomains = Collections.emptyList(); /** * Constructor @@ -81,18 +82,52 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid myValidationSupport = theValidationSupport; } - private String determineResourceName(Document theDocument) { - Element root = null; + /** + * Every element in a resource or data type includes an optional extension child element + * which is identified by it's {@code url attribute}. There exists a number of predefined + * extension urls or extension domains:
    + *
  • any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.
  • + *
  • any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.
  • + *
+ * It is possible to extend this list of known extension by defining custom extensions: + * Any url which starts which one of the elements in the list of custom extension domains is + * considered as known. + *

+ * Any unknown extension domain will result in an information message when validating a resource. + *

+ */ + public FhirInstanceValidator setCustomExtensionDomains(List extensionDomains) { + this.extensionDomains = extensionDomains; + return this; + } + /** + * Every element in a resource or data type includes an optional extension child element + * which is identified by it's {@code url attribute}. There exists a number of predefined + * extension urls or extension domains:
    + *
  • any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.
  • + *
  • any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.
  • + *
+ * It is possible to extend this list of known extension by defining custom extensions: + * Any url which starts which one of the elements in the list of custom extension domains is + * considered as known. + *

+ * Any unknown extension domain will result in an information message when validating a resource. + *

+ */ + public FhirInstanceValidator setCustomExtensionDomains(String... extensionDomains) { + this.extensionDomains = Arrays.asList(extensionDomains); + return this; + } + + private String determineResourceName(Document theDocument) { NodeList list = theDocument.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { if (list.item(i) instanceof Element) { - root = (Element) list.item(i); - break; + return list.item(i).getLocalName(); } } - root = theDocument.getDocumentElement(); - return root.getLocalName(); + return theDocument.getDocumentElement().getLocalName(); } private ArrayList determineIfProfilesSpecified(Document theDocument) { @@ -138,7 +173,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid * guielines will be ignored. *

* - * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} + * @see #setBestPracticeWarningLevel(BestPracticeWarningLevel) */ public BestPracticeWarningLevel getBestPracticeWarningLevel() { return myBestPracticeWarningLevel; @@ -235,6 +270,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); v.setResourceIdRule(IdStatus.OPTIONAL); v.setNoTerminologyChecks(isNoTerminologyChecks()); + v.addExtensionDomains(extensionDomains); List messages = new ArrayList<>(); @@ -343,7 +379,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid private LoadingCache myFetchResourceCache; private org.hl7.fhir.r4.model.Parameters myExpansionProfile; - public WorkerContextWrapper(HapiWorkerContext theWorkerContext) { + WorkerContextWrapper(HapiWorkerContext theWorkerContext) { myWrap = theWorkerContext; myConverter = new VersionConvertor_30_40(); @@ -424,7 +460,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid } @Override - public void cacheResource(org.hl7.fhir.r4.model.Resource res) throws FHIRException { + public void cacheResource(org.hl7.fhir.r4.model.Resource res) { throw new UnsupportedOperationException(); } @@ -446,7 +482,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid @Override public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ValueSet source, boolean cacheOk, boolean heiarchical) { - ValueSet convertedSource = null; + ValueSet convertedSource; try { convertedSource = VersionConvertor_30_40.convertValueSet(source); } catch (FHIRException e) { @@ -470,7 +506,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid } @Override - public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heiarchical) throws FHIRException { + public ValueSetExpander.ValueSetExpansionOutcome expandVS(org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heiarchical) { throw new UnsupportedOperationException(); } @@ -637,7 +673,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid } @Override - public IResourceValidator newValidator() throws FHIRException { + public IResourceValidator newValidator() { throw new UnsupportedOperationException(); } @@ -662,7 +698,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid } @Override - public boolean supportsSystem(String system) throws TerminologyServiceException { + public boolean supportsSystem(String system) { return myWrap.supportsSystem(system); } @@ -679,6 +715,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid @Override public ValidationResult validateCode(String system, String code, String display) { org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(system, code, display); + // TODO: converted code might be null -> NPE return convertValidationResult(result); } @@ -729,6 +766,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid throw new InternalErrorException(e); } + // TODO: converted code might be null -> NPE org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs); return convertValidationResult(result); } @@ -749,6 +787,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid throw new InternalErrorException(e); } + // TODO: converted code might be null -> NPE org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult result = myWrap.validateCode(convertedCode, convertedVs); return convertValidationResult(result); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java index 0e933a915cc..88f1a24b24e 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/hapi/validation/FhirInstanceValidator.java @@ -9,7 +9,6 @@ import ca.uhn.fhir.validation.IValidatorModule; import com.google.gson.*; import org.apache.commons.lang3.Validate; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; @@ -33,6 +32,7 @@ import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -45,6 +45,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid private DocumentBuilderFactory myDocBuilderFactory; private boolean myNoTerminologyChecks; private StructureDefinition myStructureDefintion; + private List extensionDomains = Collections.emptyList(); private IValidationSupport myValidationSupport; @@ -68,18 +69,52 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid myValidationSupport = theValidationSupport; } - private String determineResourceName(Document theDocument) { - Element root = null; + /** + * Every element in a resource or data type includes an optional extension child element + * which is identified by it's {@code url attribute}. There exists a number of predefined + * extension urls or extension domains:
    + *
  • any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.
  • + *
  • any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.
  • + *
+ * It is possible to extend this list of known extension by defining custom extensions: + * Any url which starts which one of the elements in the list of custom extension domains is + * considered as known. + *

+ * Any unknown extension domain will result in an information message when validating a resource. + *

+ */ + public FhirInstanceValidator setCustomExtensionDomains(List extensionDomains) { + this.extensionDomains = extensionDomains; + return this; + } + /** + * Every element in a resource or data type includes an optional extension child element + * which is identified by it's {@code url attribute}. There exists a number of predefined + * extension urls or extension domains:
    + *
  • any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.
  • + *
  • any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.
  • + *
+ * It is possible to extend this list of known extension by defining custom extensions: + * Any url which starts which one of the elements in the list of custom extension domains is + * considered as known. + *

+ * Any unknown extension domain will result in an information message when validating a resource. + *

+ */ + public FhirInstanceValidator setCustomExtensionDomains(String... extensionDomains) { + this.extensionDomains = Arrays.asList(extensionDomains); + return this; + } + + private String determineResourceName(Document theDocument) { NodeList list = theDocument.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { if (list.item(i) instanceof Element) { - root = (Element) list.item(i); - break; + return list.item(i).getLocalName(); } } - root = theDocument.getDocumentElement(); - return root.getLocalName(); + return theDocument.getDocumentElement().getLocalName(); } private ArrayList determineIfProfilesSpecified(Document theDocument) { @@ -120,8 +155,8 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice * guielines will be ignored. *

- * - * @see {@link #setBestPracticeWarningLevel(BestPracticeWarningLevel)} + * + * @see #setBestPracticeWarningLevel(BestPracticeWarningLevel) */ public BestPracticeWarningLevel getBestPracticeWarningLevel() { return myBestPracticeWarningLevel; @@ -211,8 +246,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); v.setResourceIdRule(IdStatus.OPTIONAL); v.setNoTerminologyChecks(isNoTerminologyChecks()); + v.addExtensionDomains(extensionDomains); - List messages = new ArrayList(); + List messages = new ArrayList<>(); if (theEncoding == EncodingEnum.XML) { Document document; @@ -312,7 +348,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid public static class NullEvaluationContext implements IEvaluationContext { @Override - public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List theParameters) throws PathEngineException { + public TypeDetails checkFunction(Object theAppContext, String theFunctionName, List theParameters) { return null; } @@ -327,12 +363,12 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid } @Override - public Base resolveConstant(Object theAppContext, String theName) throws PathEngineException { + public Base resolveConstant(Object theAppContext, String theName) { return null; } @Override - public TypeDetails resolveConstantType(Object theAppContext, String theName) throws PathEngineException { + public TypeDetails resolveConstantType(Object theAppContext, String theName) { return null; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java index a57decbc5cb..50e6fff9d89 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java @@ -21,8 +21,8 @@ import org.hl7.fhir.r4.elementmodel.ParserBase.ValidationPolicy; import org.hl7.fhir.r4.formats.FormatUtilities; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent; -import org.hl7.fhir.r4.model.ElementDefinition.*; import org.hl7.fhir.r4.model.Enumeration; +import org.hl7.fhir.r4.model.ElementDefinition.*; import org.hl7.fhir.r4.model.Enumerations.BindingStrength; import org.hl7.fhir.r4.model.ImplementationGuide.ImplementationGuideGlobalComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; @@ -416,8 +416,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } private boolean allowUnknownExtension(String url) { - if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression")) - // Added structuredefinition-expression explicitly because it wasn't defined in the version of the spec it needs to be used with + if (isPredefinedExtension(url)) return true; for (String s : extensionDomains) if (url.startsWith(s)) @@ -426,8 +425,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } private boolean isKnownExtension(String url) { - // Added structuredefinition-expression and following extensions explicitly because they weren't defined in the version of the spec they need to be used with - if (url.contains("example.org") || url.contains("acme.com") || url.contains("nema.org") || url.startsWith("http://hl7.org/fhir/tools/StructureDefinition/") || url.equals("http://hl7.org/fhir/StructureDefinition/structuredefinition-expression") || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION)) + if (isPredefinedExtension(url)) return true; for (String s : extensionDomains) if (url.startsWith(s)) @@ -435,6 +433,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return false; } + private boolean isPredefinedExtension(String url) { + return url.contains("example.org") + || url.contains("acme.com") + || url.contains("nema.org") + || url.startsWith("http://hl7.org/fhir/StructureDefinition/") + || url.equals(VersionConvertorConstants.IG_DEPENDSON_PACKAGE_EXTENSION); + } + private void bpCheck(List errors, IssueType invalid, int line, int col, String literalPath, boolean test, String message) { if (bpWarnings != null) { switch (bpWarnings) { @@ -1950,6 +1956,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return extensionDomains; } + public InstanceValidator addExtensionDomains(List extensionDomains) { + this.extensionDomains.addAll(extensionDomains); + return this; + } + private Element getFromBundle(Element bundle, String ref, String fullUrl, List errors, String path) { String targetUrl = null; String version = ""; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireValidatorDstu3Test.java new file mode 100644 index 00000000000..a85fe71d9b8 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireValidatorDstu3Test.java @@ -0,0 +1,133 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.ValidationResult; +import org.hamcrest.Matchers; +import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; +import org.hl7.fhir.dstu3.model.Questionnaire; +import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +public class QuestionnaireValidatorDstu3Test { + private static final Logger ourLog = LoggerFactory.getLogger(QuestionnaireValidatorDstu3Test.class); + private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); + private static FhirContext ourCtx = FhirContext.forDstu3(); + private FhirInstanceValidator myInstanceVal; + private FhirValidator myVal; + + @Before + public void before() { + IValidationSupport myValSupport = mock(IValidationSupport.class); + + myVal = ourCtx.newValidator(); + myVal.setValidateAgainstStandardSchema(false); + myVal.setValidateAgainstStandardSchematron(false); + + ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport); + myInstanceVal = new FhirInstanceValidator(validationSupport); + + myVal.registerValidatorModule(myInstanceVal); + } + + @Test + public void testQuestionnaireWithPredefinedExtensionDomainsForCoding() { + String[] extensionDomainsToTest = new String[] { + "http://example.org/questionnaire-color-control-1", + "https://example.org/questionnaire-color-control-2", + "http://acme.com/questionnaire-color-control-3", + "https://acme.com/questionnaire-color-control-4", + "http://nema.org/questionnaire-color-control-5", + "https://nema.org/questionnaire-color-control-6", + "http://hl7.org/fhir/StructureDefinition/questionnaire-scoreItem", + "http://hl7.org/fhir/StructureDefinition/structuredefinition-expression", + }; + for (String extensionDomainToTest : extensionDomainsToTest) { + Questionnaire q = new Questionnaire(); + q.setStatus(PublicationStatus.ACTIVE) + .addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionDomainToTest) + .setValue(new Coding(null, "text-box", null)); + + ValidationResult errors = myVal.validateWithResult(q); + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + } + + @Test + public void testQuestionnaireWithPredefinedExtensionDomainsForCodeableConcept() { + String[] extensionDomainsToTest = new String[] { + "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + }; + for (String extensionDomainToTest : extensionDomainsToTest) { + Questionnaire q = new Questionnaire(); + q.setStatus(PublicationStatus.ACTIVE) + .addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionDomainToTest) + .setValue(new CodeableConcept().addCoding(new Coding(null, "text-box", null))); + + ValidationResult errors = myVal.validateWithResult(q); + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + } + + @Test + public void testQuestionnaireWithCustomExtensionDomain() { + Questionnaire q = new Questionnaire(); + String extensionUrl = "http://my.own.domain/StructureDefinition/"; + q.setStatus(PublicationStatus.ACTIVE) + .addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionUrl + "questionnaire-itemControl") + .setValue(new Coding(null, "text-box", null)); + + ValidationResult errors = myVal.validateWithResult(q); + + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.hasSize(1)); + assertEquals(errors.getMessages().get(0).getSeverity(), ResultSeverityEnum.INFORMATION); + assertThat(errors.getMessages().get(0).getMessage(), Matchers.startsWith("Unknown extension " + extensionUrl)); + + myInstanceVal.setCustomExtensionDomains(Collections.singletonList(extensionUrl)); + errors = myVal.validateWithResult(q); + + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + + @AfterClass + public static void afterClassClearContext() { + myDefaultValidationSupport.flush(); + myDefaultValidationSupport = null; + TestUtil.clearAllStaticFieldsForUnitTest(); + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireValidatorR4Test.java new file mode 100644 index 00000000000..6c9334bf76e --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireValidatorR4Test.java @@ -0,0 +1,144 @@ +package org.hl7.fhir.r4.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; +import ca.uhn.fhir.validation.ValidationResult; +import org.hamcrest.Matchers; +import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; +import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; +import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.r4.hapi.validation.ValidationSupportChain; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Narrative; +import org.hl7.fhir.r4.model.Narrative.NarrativeStatus; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +public class QuestionnaireValidatorR4Test { + private static final Logger ourLog = LoggerFactory.getLogger(QuestionnaireValidatorR4Test.class); + private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); + private static FhirContext ourCtx = FhirContext.forR4(); + private FhirInstanceValidator myInstanceVal; + private FhirValidator myVal; + + @Before + public void before() { + IValidationSupport myValSupport = mock(IValidationSupport.class); + + myVal = ourCtx.newValidator(); + myVal.setValidateAgainstStandardSchema(false); + myVal.setValidateAgainstStandardSchematron(false); + + ValidationSupportChain validationSupport = new ValidationSupportChain(myValSupport, myDefaultValidationSupport); + myInstanceVal = new FhirInstanceValidator(validationSupport); + + myVal.registerValidatorModule(myInstanceVal); + } + + @Test + public void testQuestionnaireWithPredefinedExtensionDomains() { + String[] extensionDomainsToTest = new String[] { + "http://example.org/questionnaire-color-control-1", + "https://example.org/questionnaire-color-control-2", + "http://acme.com/questionnaire-color-control-3", + "https://acme.com/questionnaire-color-control-4", + "http://nema.org/questionnaire-color-control-5", + "https://nema.org/questionnaire-color-control-6", + "http://hl7.org/fhir/StructureDefinition/questionnaire-scoreItem", + "http://hl7.org/fhir/StructureDefinition/structuredefinition-expression", + + }; + for (String extensionDomainToTest : extensionDomainsToTest) { + Questionnaire q = minimalValidQuestionnaire(); + q.addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionDomainToTest) + .setValue(new Coding(null, "text-box", null)); + + ValidationResult errors = myVal.validateWithResult(q); + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + } + + @Test + public void testQuestionnaireWithPredefinedExtensionDomainsForCodeableConcept() { + String[] extensionDomainsToTest = new String[] { + "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + }; + for (String extensionDomainToTest : extensionDomainsToTest) { + Questionnaire q = minimalValidQuestionnaire(); + q.addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionDomainToTest) + .setValue(new CodeableConcept().addCoding(new Coding(null, "text-box", null))); + + ValidationResult errors = myVal.validateWithResult(q); + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + } + + @Test + public void testQuestionnaireWithCustomExtensionDomain() { + String extensionUrl = "http://my.own.domain/StructureDefinition/"; + Questionnaire q = minimalValidQuestionnaire(); + q.addItem() + .setLinkId("link0") + .setType(QuestionnaireItemType.STRING) + .addExtension() + .setUrl(extensionUrl + "questionnaire-itemControl") + .setValue(new Coding(null, "text-box", null)); + + ValidationResult errors = myVal.validateWithResult(q); + + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.hasSize(1)); + assertEquals(errors.getMessages().get(0).getSeverity(), ResultSeverityEnum.INFORMATION); + assertThat(errors.getMessages().get(0).getMessage(), Matchers.startsWith("Unknown extension " + extensionUrl)); + + myInstanceVal.setCustomExtensionDomains(extensionUrl); + errors = myVal.validateWithResult(q); + + ourLog.info(errors.toString()); + assertThat(errors.isSuccessful(), Matchers.is(true)); + assertThat(errors.getMessages(), Matchers.empty()); + } + + private Questionnaire minimalValidQuestionnaire() { + Narrative n = new Narrative().setStatus(NarrativeStatus.GENERATED); + n.setDivAsString("simple example"); + Questionnaire q = new Questionnaire(); + q.setText(n); + q.setName("SomeName"); + q.setStatus(PublicationStatus.ACTIVE); + return q; + } + + @AfterClass + public static void afterClassClearContext() { + myDefaultValidationSupport.flush(); + myDefaultValidationSupport = null; + TestUtil.clearAllStaticFieldsForUnitTest(); + } +} diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/AbstractGeneratorMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/AbstractGeneratorMojo.java new file mode 100644 index 00000000000..113b8eaaad2 --- /dev/null +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/AbstractGeneratorMojo.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.tinder; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import java.io.File; +import java.util.List; + +/** + * Base class for mojo generatorss. + */ +public abstract class AbstractGeneratorMojo extends AbstractMojo { + + protected final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(getClass()); + + @Parameter(required = true, defaultValue = "${project.build.directory}/..") + protected String baseDir; + + @Parameter + protected String packageBase = ""; + + @Parameter + protected List baseResourceNames; + + @Parameter + protected List excludeResourceNames; + + @Parameter + protected String templateName; + + @Parameter(required = true) + protected String version; + + @Component + protected MavenProject myProject; + + @Override + public final void execute() throws MojoExecutionException, MojoFailureException { + doExecute(new Configuration(this.version, baseDir, getTargetDirectory(), this.packageBase, this.baseResourceNames, this.excludeResourceNames)); + } + + protected abstract void doExecute(Configuration mavenGeneratorConfiguration) throws MojoExecutionException, MojoFailureException; + + protected abstract File getTargetDirectory(); + +} diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java new file mode 100644 index 00000000000..b6034ceefda --- /dev/null +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java @@ -0,0 +1,134 @@ +package ca.uhn.fhir.tinder; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser; +import org.apache.commons.lang.WordUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.TreeSet; + +public class Configuration { + + private final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(Configuration.class); + + private String version; + private File targetDirectory; + private String packageSuffix; + + private String packageBase; + private FhirContext fhirContext; + private File packageDirectoryBase; + + private final List resourceNames = new ArrayList<>(); + private String baseDir; + + public Configuration(String version, String baseDir, File targetDirectory, String packageBase, List baseResourceNames, List excludeResourceNames) { + this.targetDirectory = targetDirectory; + this.packageBase = packageBase; + this.packageDirectoryBase = new File(targetDirectory, packageBase.replace(".", File.separatorChar + "")); + + switch (version) { + case "dstu2": + fhirContext = FhirContext.forDstu2(); + break; + case "dstu3": + fhirContext = FhirContext.forDstu3(); + packageSuffix = ".dstu3"; + break; + case "r4": + fhirContext = FhirContext.forR4(); + packageSuffix = ".r4"; + break; + default: + throw new IllegalArgumentException("Unknown version configured: " + version); + } + + this.version = version; + if (baseResourceNames == null || baseResourceNames.isEmpty()) { + ourLog.info("No resource names supplied, going to use all resources from version: {}", fhirContext.getVersion().getVersion()); + + Properties p = new Properties(); + try { + p.load(fhirContext.getVersion().getFhirVersionPropertiesFile()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to load version property file", e); + } + + ourLog.debug("Property file contains: {}", p); + + TreeSet keys = new TreeSet(); + for (Object next : p.keySet()) { + keys.add((String) next); + } + for (String next : keys) { + if (next.startsWith("resource.")) { + resourceNames.add(next.substring("resource.".length()).toLowerCase()); + } + } + + if (fhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) { + resourceNames.remove("conformance"); + } + } else { + for (String resourceName : baseResourceNames) { + resourceNames.add(resourceName.toLowerCase()); + } + } + + if (excludeResourceNames != null) { + for (String resourceName : excludeResourceNames) { + resourceNames.remove(resourceName.toLowerCase()); + } + } + + ourLog.info("Including the following resources: {}", resourceNames); + } + + public File getPackageDirectoryBase() { + return packageDirectoryBase; + } + + public String getPackageSuffix() { + return packageSuffix; + } + + public List getResourceNames() { + return resourceNames; + } + + public String getPackageBase() { + return packageBase; + } + + public String getVersion() { + return version; + } + + public String getResourcePackage() { + if (BaseStructureSpreadsheetParser.determineVersionEnum(version).isRi()) { + return "org.hl7.fhir." + version + ".model"; + } + return "ca.uhn.fhir.model." + version + ".resource"; + } + + public String getVersionCapitalized() { + String capitalize = WordUtils.capitalize(version); + if ("Dstu".equals(capitalize)) { + return "Dstu1"; + } + return capitalize; + } + + public File getTargetDirectory() { + return targetDirectory; + } + + public String getBaseDir() { + return baseDir; + } +} diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderResourceGeneratorMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderResourceGeneratorMojo.java new file mode 100644 index 00000000000..2aadac24a13 --- /dev/null +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderResourceGeneratorMojo.java @@ -0,0 +1,75 @@ +package ca.uhn.fhir.tinder; + +import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingModel; +import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingSpreadsheet; +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.tools.generic.EscapeTool; + +import java.io.*; + +@Mojo(name = "generate-resource", defaultPhase = LifecyclePhase.GENERATE_RESOURCES) +public class TinderResourceGeneratorMojo extends AbstractGeneratorMojo { + + @Parameter(required = true, defaultValue = "${project.build.directory}/generated-resources/tinder") + protected File targetDirectory; + + @Parameter(required = true) + protected String fileName = ""; + + @Override + protected void doExecute(Configuration configuration) throws MojoExecutionException, MojoFailureException { + File packageDirectoryBase = configuration.getPackageDirectoryBase(); + packageDirectoryBase.mkdirs(); + + ResourceGeneratorUsingModel gen = new ResourceGeneratorUsingModel(configuration.getVersion(), configuration.getBaseDir()); + gen.setBaseResourceNames(configuration.getResourceNames()); + + try { + gen.parse(); + + VelocityContext ctx = new VelocityContext(); + ctx.put("resources", gen.getResources()); + ctx.put("packageBase", configuration.getPackageBase()); + ctx.put("version", configuration.getVersion()); + ctx.put("package_suffix", configuration.getPackageSuffix()); + ctx.put("esc", new EscapeTool()); + + ctx.put("resourcePackage", configuration.getResourcePackage()); + ctx.put("versionCapitalized", configuration.getVersionCapitalized()); + + VelocityEngine v = new VelocityEngine(); + v.setProperty("resource.loader", "cp"); + v.setProperty("cp.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + v.setProperty("runtime.references.strict", Boolean.TRUE); + + InputStream templateIs = ResourceGeneratorUsingSpreadsheet.class.getResourceAsStream(templateName); + InputStreamReader templateReader = new InputStreamReader(templateIs); + + File file = new File(packageDirectoryBase, fileName); + OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(file, false), "UTF-8"); + v.evaluate(ctx, w, "", templateReader); + w.close(); + + Resource resource = new Resource(); + resource.setDirectory(packageDirectoryBase.getAbsolutePath()); + //resource.setDirectory(targetDirectory.getAbsolutePath()); + //resource.addInclude(packageBase); + myProject.addResource(resource); + + } catch (Exception e) { + throw new MojoFailureException("Failed to generate resources", e); + } + } + + @Override + public File getTargetDirectory() { + return targetDirectory; + } +} diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderSourcesGeneratorMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderSourcesGeneratorMojo.java new file mode 100644 index 00000000000..c93e7fbb41b --- /dev/null +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderSourcesGeneratorMojo.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.tinder; + +import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingModel; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; + +@Mojo(name = "generate-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES) +public class TinderSourcesGeneratorMojo extends AbstractGeneratorMojo { + + @Parameter(required = true, defaultValue = "${project.build.directory}/generated-sources/tinder") + protected File targetDirectory; + + @Parameter + private String filenameSuffix = "ResourceProvider"; + + @Parameter + private String filenamePrefix = ""; + + @Override + public void doExecute(Configuration configuration) throws MojoExecutionException, MojoFailureException { + File packageDirectoryBase = configuration.getPackageDirectoryBase(); + packageDirectoryBase.mkdirs(); + + ResourceGeneratorUsingModel gen = new ResourceGeneratorUsingModel(configuration.getVersion(), configuration.getBaseDir()); + gen.setBaseResourceNames(configuration.getResourceNames()); + + try { + gen.parse(); + + gen.setFilenameSuffix(filenameSuffix); + gen.setFilenamePrefix(filenamePrefix); + gen.setTemplate(templateName); + gen.writeAll(packageDirectoryBase, null, configuration.getPackageBase()); + } catch (Exception e) { + throw new MojoFailureException("Failed to generate server", e); + } + + myProject.addCompileSourceRoot(configuration.getTargetDirectory().getAbsolutePath()); + } + + @Override + protected File getTargetDirectory() { + return targetDirectory; + } +} diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java index 63f9d423d23..f51a1147e7b 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java @@ -711,7 +711,7 @@ public abstract class BaseStructureParser { return null; } - public static FhirVersionEnum determineVersionEnum(String version) throws MojoFailureException { + public static FhirVersionEnum determineVersionEnum(String version) { FhirVersionEnum versionEnum; if ("dstu2".equals(version)) { versionEnum = FhirVersionEnum.DSTU2; @@ -720,7 +720,7 @@ public abstract class BaseStructureParser { } else if ("r4".equals(version)) { versionEnum = FhirVersionEnum.R4; } else { - throw new MojoFailureException("Unknown version: " + version); + throw new IllegalArgumentException("Unknown version: " + version); } return versionEnum; } diff --git a/pom.xml b/pom.xml index 8b512e78864..06d430736ca 100644 --- a/pom.xml +++ b/pom.xml @@ -482,6 +482,9 @@ magnuswatn Magnus Watn + + Cory00 + @@ -515,20 +518,21 @@ 25.0-jre 2.8.5 2.2.11_1 - 2.3.0 - 2.3.0 + 2.3.1 + 2.3.0.1 + 2.3.1 2.25.1 - 9.4.12.v20180830 + 9.4.14.v20181114 3.0.2 - 5.3.6.Final - 5.10.3.Final - 5.4.1.Final + 5.4.0.Final + 5.11.0.Final + 5.5.5 + 5.4.1.Final 4.4.6 4.5.3 2.9.7 - 5.5.5 2.5.3 1.8 4.0.0.Beta3 @@ -540,10 +544,10 @@ 1.7.25 5.1.3.RELEASE 2.1.3.RELEASE - 1.5.6.RELEASE + 2.1.1.RELEASE 3.1.4 - 3.0.9.RELEASE + 3.0.11.RELEASE 4.4.1 @@ -660,6 +664,11 @@ javax.activation 1.2.0
+ + com.sun.activation + jakarta.activation + 1.2.1 + com.sun.mail javax.mail @@ -752,6 +761,7 @@ mssql-jdbc 6.2.2.jre8 + javax.mail javax.mail-api @@ -780,7 +791,7 @@ javax.validation validation-api - 1.1.0.Final + 2.0.1.Final javax.ws.rs @@ -1061,13 +1072,11 @@ javax.json 1.0.4 - org.glassfish.jersey.core jersey-server @@ -1151,7 +1160,7 @@ org.mockito mockito-core - 2.18.3 + 2.23.4 org.postgresql @@ -1320,7 +1329,7 @@ org.basepom.maven duplicate-finder-maven-plugin - 1.2.1 + 1.3.0 de.jpdigital @@ -1484,7 +1493,7 @@ org.codehaus.mojo versions-maven-plugin - 2.6 + 2.7 false diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 06a29cf65c1..da45a93af41 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -12,9 +12,15 @@ latest versions (dependent HAPI modules listed in brackets): -
  • Spring (JPA): 5.0.8 -> 5.1.3
  • -
  • Spring Data (JPA): 2.0.7 -> 2.1.3
  • +
  • Spring (JPA): 5.0.8.RELEASE -> 5.1.3.RELEASE
  • +
  • Spring-Data (JPA): 2.0.7.RELEASE -> 2.1.3.RELEASE
  • +
  • Hibernate-Core (JPA): 5.3.6.FINAL -> 5.4.0.FINAL
  • +
  • Hibernate-Search (JPA): 5.10.3.FINAL -> 5.11.0.FINAL
  • +
  • Thymeleaf (JPA): 3.0.9.RELEASE -> 3.0.11.RELEASE
  • thymeleaf-spring4 (Testpage Overlay) has been replaced with thymeleaf-spring5
  • +
  • Commons-Lang3: 3.8 -> 3.8.1
  • +
  • Commons-Text: 1.4 -> 1.4
  • +
  • Spring Boot: 1.5.6.RELEASE -> 2.1.1.RELEASE
  • ]]> @@ -27,6 +33,23 @@ DaoConfig (by default it is true). If isEnableInMemorySubscriptionMatching is "false", then all subscription matching will query the database as before. + + Removed BaseSubscriptionInterceptor and all its subclasses (RestHook, EMail, WebSocket). These are replaced + by two new interceptors: SubscriptionActivatingInterceptor that is responsible for activating subscriptions + and SubscriptionMatchingInterceptor that is responsible for matching incoming resources against activated + subscriptions. Call DaoConfig.addSupportedSubscriptionType(type) to configure which subscription types + are supported in your environment. The helper method SubscriptionInterceptorLoader.registerInterceptors() + will check if any subscription types are supported, and if so then register both the activating and matching + interceptors. See https://github.com/jamesagnew/hapi-fhir/wiki/Proposed-Subscription-Design-Change for more + details. + + + Added support for matching subscriptions in a separate server from the REST Server. To do this, run the + SubscriptionActivatingInterceptor on the REST server and the SubscriptionMatchingInterceptor in the + standalone server. Classes required to support running a standalone subscription server are in the + ca.uhn.fhir.jpa.subscription.module.standalone package. These classes are excluded by default from + the JPA ApplicationContext (that package is explicitly filtered out in the BaseConfig.java @ComponentScan). + Changed behaviour of FHIR Server to reject subscriptions with invalid criteria. If a Subscription is submitted with invalid criteria, the server returns HTTP 422 "Unprocessable Entity" and the @@ -100,9 +123,11 @@ or authorizing the contents of the response. + JPA Migrator tool enhancements: An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL - statements will be logged at the end of the run. + statements will be logged at the end of the run. Also, a case sensitivity issue when running against + some Postgres databases has been corrected. In the JPA server, when performing a chained reference search on a search parameter with @@ -134,6 +159,68 @@ An issue was corrected with the JPA reindexer, where String index columns do not always get reindexed if they did not have an identity hash value in the HASH_IDENTITY column. + + Plain Server ResourceProvider classes are no longer required to be public classes. This + limitation has always been enforced, but did not actually serve any real purpose so it + has been removed. + + + A new interceptor called ServeMediaResourceRawInterceptor has been added. This interceptor + causes Media resources to be served as raw content if the client explicitly requests + the correct content type cia the Accept header. + + + A new configuration item has been added to the FhirInstanceValidator that + allows you to specify additional "known extension domains", meaning + domains in which the validator will not complain about when it + encounters new extensions. Thanks to Heinz-Dieter Conradi for the + pull request! + + + Under some circumstances, when a custom search parameter was added to the JPA server + resources could start reindexing before the new search parameter had been saved, meaning that + it was not applied to all resources. This has been corrected. + + + In example-projects/README.md and hapi-fhir-jpaserver-example/README.md, incidate that these examples projects + are no longer maintained. The README.md points users to a starter project they should use for examples. + + + Replaced use of BeanFactory with custom factory classes that Spring @Lookup the @Scope("prototype") beans + (e.g. SearchBuilderFactory). + + + Moved e-mail from address configuration from EmailInterceptor (which doesn't exist any more) to DaoConfig. + + + Added 3 interfaces for services required by the standalone subscription server. The standalone subscription + server doesn't have access to a database and so needs to get its resources using a FhirClient. Thus + for each of these interfaces, there are two implementations: a Dao implementaiton and a FhirClient + implementation. The interfaces thus introduced are ISubscriptionProvider (used to load subscriptions + into the SubscriptionRegistry), the IResourceProvider (used to get the latest version of a resource + if the "get latest version" flag is set on the subscription) and ISearchParamProvider used to load + custom search parameters. + + + Separated active subscription cache from the interceptors into a new Spring component called the + SubscriptionRegistry. This component maintains a cache of ActiveSubscriptions. An ActiveSubscription + contains the subscription, it's delivery channel, and a list of delivery handlers. + + + Introduced a Spring factory interfaces called ISubscriptionDeliveryChannelFactory and + ISubscriptionDeliveryHandlerFactory that are used to create delivery channels and handlers. By default, + HAPI FHIR ships with a LinkedBlockingQueue implementation of the delivery channel factory. If a different + type of channel factory is required, add it to your application context and mark it as @Primary. + + + When using the HL7.org DSTU2 structures, a QuestionnaireResponse with a + value of type reference would fail to parse. Thanks to David Gileadi for + the pull request! + + + FHIR Servers now support the HTTP HEAD method for FHIR read operations. Thanks + to GitHub user Cory00 for the pull request! +