From ad3d5489ea2fe31b090c9c09a69dc28a797038da Mon Sep 17 00:00:00 2001 From: Jeff Chung Date: Fri, 16 Jun 2017 14:25:57 -0700 Subject: [PATCH 1/2] Fixed FHIR subscriptions with criteria previously limited to 10, added fhir criteria validation before create/update, added dstu2 configurations --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 2 +- .../RestHookSubscriptionDstu2Interceptor.java | 70 +++++++- .../RestHookSubscriptionDstu3Interceptor.java | 82 +++++++-- .../fhir/jpa/demo/FhirServerConfigDstu2.java | 125 +++++++++++++ .../fhir/jpa/demo/FhirTesterConfigDstu2.java | 56 ++++++ .../uhn/fhir/jpa/demo/JpaServerDemoDstu2.java | 169 ++++++++++++++++++ ...g-subscription-dstu2.xml => web-dstu2.xml} | 8 +- .../web-non-polling-subscription-dstu3.xml | 108 ----------- .../web-polling-subscription-dstu2.xml | 108 ----------- 9 files changed, 493 insertions(+), 235 deletions(-) create mode 100644 hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java create mode 100644 hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu2.java create mode 100644 hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java rename hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/{web-non-polling-subscription-dstu2.xml => web-dstu2.xml} (94%) delete mode 100644 hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml delete mode 100644 hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml 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 dd71d3920cb..fcac59d05b4 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 @@ -2046,7 +2046,7 @@ public abstract class BaseHapiFhirDao implements IDao { return paramMap; } - protected static List translateMatchUrl(String theMatchUrl) { + public static List translateMatchUrl(String theMatchUrl) { List parameters; String matchUrl = theMatchUrl; int questionMarkIndex = matchUrl.indexOf('?'); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu2Interceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu2Interceptor.java index ea4f73bfc38..cf33f7d0a11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu2Interceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu2Interceptor.java @@ -29,9 +29,12 @@ import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -63,6 +66,7 @@ import ca.uhn.fhir.rest.server.interceptor.*; public class RestHookSubscriptionDstu2Interceptor extends ServerOperationInterceptorAdapter { + private static final Integer MAX_SUBSCRIPTION_RESULTS = 10000; private static volatile ExecutorService executor; private final static int MAX_THREADS = 1; @@ -71,13 +75,13 @@ public class RestHookSubscriptionDstu2Interceptor extends ServerOperationInterce @Autowired private FhirContext myFhirContext; - private boolean myNotifyOnDelete = false; - private final List myRestHookSubscriptions = new ArrayList(); - @Autowired @Qualifier("mySubscriptionDaoDstu2") private IFhirResourceDao mySubscriptionDao; + private boolean myNotifyOnDelete = false; + private final List myRestHookSubscriptions = new ArrayList(); + /** * Check subscriptions and send notifications or payload * @@ -205,6 +209,7 @@ public class RestHookSubscriptionDstu2Interceptor extends ServerOperationInterce req.setSubRequest(true); IFhirResourceDao responseDao = mySubscriptionDao.getDao(responseResourceDef.getImplementingClass()); + responseCriteriaUrl.setCount(MAX_SUBSCRIPTION_RESULTS); IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); return responseResults; } @@ -265,7 +270,12 @@ public class RestHookSubscriptionDstu2Interceptor extends ServerOperationInterce RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); + map.setCount(MAX_SUBSCRIPTION_RESULTS); IBundleProvider subscriptionBundleList = mySubscriptionDao.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()); for (IBaseResource resource : resourceList) { @@ -398,4 +408,58 @@ public class RestHookSubscriptionDstu2Interceptor extends ServerOperationInterce public void setSubscriptionDao(IFhirResourceDao theSubscriptionDao) { mySubscriptionDao = theSubscriptionDao; } + + @Override + public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theDetails) { + //check the subscription criteria to see if its valid before creating or updating a subscription + if (RestOperationTypeEnum.CREATE.equals(theOperation) || RestOperationTypeEnum.UPDATE.equals(theOperation)) { + String resourceType = theDetails.getResourceType(); + ourLog.info("prehandled resource type: " + resourceType); + if (resourceType != null && resourceType.equals(Subscription.class.getSimpleName())) { + Subscription subscription = (Subscription) theDetails.getResource(); + if (subscription != null) { + checkSubscriptionCriterias(subscription); + } + } + } + super.incomingRequestPreHandled(theOperation, theDetails); + } + + private void checkSubscriptionCriterias(Subscription subscription){ + try { + IBundleProvider results = executeSubscriptionCriteria(subscription, null); + }catch (Exception e){ + throw new InvalidRequestException("Invalid criteria"); + } + } + + private IBundleProvider executeSubscriptionCriteria(Subscription subscription, IIdType idType){ + //run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource + String criteria = subscription.getCriteria(); + if(idType != null) { + criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); + } + + IBundleProvider results = getBundleProvider(criteria); + return results; + } + + /** + * Get the encoding from the criteria or return JSON encoding if its not found + * + * @param criteria + * @return + */ + private EncodingEnum getEncoding(String criteria) { + //check criteria + String params = criteria.substring(criteria.indexOf('?') + 1); + List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); + for (NameValuePair nameValuePair : paramValues) { + if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { + return EncodingEnum.forContentType(nameValuePair.getValue()); + } + } + return EncodingEnum.JSON; + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java index 449a7535802..757411f4125 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/RestHookSubscriptionDstu3Interceptor.java @@ -30,9 +30,12 @@ import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.hl7.fhir.dstu3.model.Subscription; @@ -61,6 +64,7 @@ import ca.uhn.fhir.rest.server.interceptor.*; public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterceptorAdapter { + private static final Integer MAX_SUBSCRIPTION_RESULTS = 10000; private static volatile ExecutorService executor; private final static int MAX_THREADS = 1; @@ -68,21 +72,12 @@ public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterce @Autowired private FhirContext myFhirContext; - - public void setFhirContext(FhirContext theFhirContext) { - myFhirContext = theFhirContext; - } - - public void setSubscriptionDao(IFhirResourceDao theSubscriptionDao) { - mySubscriptionDao = theSubscriptionDao; - } @Autowired @Qualifier("mySubscriptionDaoDstu3") private IFhirResourceDao mySubscriptionDao; private boolean notifyOnDelete = false; - private final List myRestHookSubscriptions = new ArrayList(); /** @@ -205,6 +200,7 @@ public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterce req.setSubRequest(true); IFhirResourceDao responseDao = mySubscriptionDao.getDao(responseResourceDef.getImplementingClass()); + responseCriteriaUrl.setCount(MAX_SUBSCRIPTION_RESULTS); IBundleProvider responseResults = responseDao.search(responseCriteriaUrl, req); return responseResults; } @@ -254,7 +250,6 @@ public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterce return entity; } - /** * Read the existing subscriptions from the database */ @@ -266,7 +261,12 @@ public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterce RequestDetails req = new ServletSubRequestDetails(); req.setSubRequest(true); + map.setCount(MAX_SUBSCRIPTION_RESULTS); IBundleProvider subscriptionBundleList = mySubscriptionDao.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()); for (IBaseResource resource : resourceList) { @@ -388,7 +388,69 @@ public class RestHookSubscriptionDstu3Interceptor extends ServerOperationInterce } } + public void setFhirContext(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + public void setNotifyOnDelete(boolean notifyOnDelete) { this.notifyOnDelete = notifyOnDelete; } + + public void setSubscriptionDao(IFhirResourceDao theSubscriptionDao) { + mySubscriptionDao = theSubscriptionDao; + } + + @Override + public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theDetails) { + //check the subscription criteria to see if its valid before creating or updating a subscription + if (RestOperationTypeEnum.CREATE.equals(theOperation) || RestOperationTypeEnum.UPDATE.equals(theOperation)) { + String resourceType = theDetails.getResourceType(); + ourLog.info("prehandled resource type: " + resourceType); + if (resourceType != null && resourceType.equals(Subscription.class.getSimpleName())) { + Subscription subscription = (Subscription) theDetails.getResource(); + if (subscription != null) { + checkSubscriptionCriterias(subscription); + } + } + } + super.incomingRequestPreHandled(theOperation, theDetails); + } + + private void checkSubscriptionCriterias(Subscription subscription){ + try { + IBundleProvider results = executeSubscriptionCriteria(subscription, null); + }catch (Exception e){ + throw new InvalidRequestException("Invalid criteria"); + } + } + + private IBundleProvider executeSubscriptionCriteria(Subscription subscription, IIdType idType){ + //run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource + String criteria = subscription.getCriteria(); + if(idType != null) { + criteria += "&_id=" + idType.getResourceType() + "/" + idType.getIdPart(); + } + + IBundleProvider results = getBundleProvider(criteria); + return results; + } + + /** + * Get the encoding from the criteria or return JSON encoding if its not found + * + * @param criteria + * @return + */ + private EncodingEnum getEncoding(String criteria) { + //check criteria + String params = criteria.substring(criteria.indexOf('?') + 1); + List paramValues = URLEncodedUtils.parse(params, Constants.CHARSET_UTF8, '&'); + for (NameValuePair nameValuePair : paramValues) { + if (Constants.PARAM_FORMAT.equals(nameValuePair.getName())) { + return EncodingEnum.forContentType(nameValuePair.getValue()); + } + } + return EncodingEnum.JSON; + } + } diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java new file mode 100644 index 00000000000..0d45ad6b25d --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu2.java @@ -0,0 +1,125 @@ +package ca.uhn.fhir.jpa.demo; + +import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; +import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2; +import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.util.Properties; + +/** + * This is the primary configuration file for the example server + */ +@Configuration +@EnableTransactionManagement() +public class FhirServerConfigDstu2 extends BaseJavaConfigDstu2 { + + /** + * Configure FHIR properties around the the JPA server via this bean + */ + @Bean() + public DaoConfig daoConfig() { + DaoConfig retVal = new DaoConfig(); + retVal.setSubscriptionEnabled(true); + retVal.setSubscriptionPollDelay(5000); + retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); + retVal.setAllowMultipleDelete(true); + return retVal; + } + + /** + * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a + * directory called "jpaserver_derby_files". + * + * A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource. + */ + @Bean(destroyMethod = "close") + public DataSource dataSource() { + BasicDataSource retVal = new BasicDataSource(); + retVal.setDriver(new org.apache.derby.jdbc.EmbeddedDriver()); + retVal.setUrl("jdbc:derby:directory:target/jpaserver_derby_files;create=true"); + retVal.setUsername(""); + retVal.setPassword(""); + return retVal; + } + + @Bean() + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); + retVal.setPersistenceUnitName("HAPI_PU"); + retVal.setDataSource(dataSource()); + retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); + retVal.setPersistenceProvider(new HibernatePersistenceProvider()); + retVal.setJpaProperties(jpaProperties()); + return retVal; + } + + private Properties jpaProperties() { + Properties extraProperties = new Properties(); + extraProperties.put("hibernate.dialect", org.hibernate.dialect.DerbyTenSevenDialect.class.getName()); + extraProperties.put("hibernate.format_sql", "true"); + extraProperties.put("hibernate.show_sql", "false"); + extraProperties.put("hibernate.hbm2ddl.auto", "update"); + extraProperties.put("hibernate.jdbc.batch_size", "20"); + extraProperties.put("hibernate.cache.use_query_cache", "false"); + extraProperties.put("hibernate.cache.use_second_level_cache", "false"); + extraProperties.put("hibernate.cache.use_structured_entries", "false"); + extraProperties.put("hibernate.cache.use_minimal_puts", "false"); + extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); + extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); + extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); +// extraProperties.put("hibernate.search.default.worker.execution", "async"); + return extraProperties; + } + + /** + * Do some fancy logging to create a nice access log that has details about each incoming request. + */ + public IServerInterceptor loggingInterceptor() { + LoggingInterceptor retVal = new LoggingInterceptor(); + retVal.setLoggerName("fhirtest.access"); + retVal.setMessageFormat( + "Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}]"); + retVal.setLogExceptions(true); + retVal.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); + return retVal; + } + + /** + * This interceptor adds some pretty syntax highlighting in responses when a browser is detected + */ + @Bean(autowire = Autowire.BY_TYPE) + public IServerInterceptor responseHighlighterInterceptor() { + ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); + return retVal; + } + + @Bean(autowire = Autowire.BY_TYPE) + public IServerInterceptor subscriptionSecurityInterceptor() { + SubscriptionsRequireManualActivationInterceptorDstu2 retVal = new SubscriptionsRequireManualActivationInterceptorDstu2(); + return retVal; + } + + @Bean() + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager retVal = new JpaTransactionManager(); + retVal.setEntityManagerFactory(entityManagerFactory); + return retVal; + } + +} diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu2.java new file mode 100644 index 00000000000..7ea3736a840 --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfigDstu2.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.jpa.demo; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.to.FhirTesterMvcConfig; +import ca.uhn.fhir.to.TesterConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +//@formatter:off + +/** + * This spring config file configures the web testing module. It serves two + * purposes: + * 1. It imports FhirTesterMvcConfig, which is the spring config for the + * tester itself + * 2. It tells the tester which server(s) to talk to, via the testerConfig() + * method below + */ +@Configuration +@Import(FhirTesterMvcConfig.class) +public class FhirTesterConfigDstu2 { + + /** + * This bean tells the testing webpage which servers it should configure itself + * to communicate with. In this example we configure it to talk to the local + * server, as well as one public server. If you are creating a project to + * deploy somewhere else, you might choose to only put your own server's + * address here. + * + * Note the use of the ${serverBase} variable below. This will be replaced with + * the base URL as reported by the server itself. Often for a simple Tomcat + * (or other container) installation, this will end up being something + * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are + * deploying your server to a place with a fully qualified domain name, + * you might want to use that instead of using the variable. + */ + @Bean + public TesterConfig testerConfig() { + TesterConfig retVal = new TesterConfig(); + retVal + .addServer() + .withId("home") + .withFhirVersion(FhirVersionEnum.DSTU2) + .withBaseUrl("${serverBase}/baseDstu2") + .withName("Local Tester") + .addServer() + .withId("hapi") + .withFhirVersion(FhirVersionEnum.DSTU2) + .withBaseUrl("http://fhirtest.uhn.ca/baseDstu2") + .withName("Public HAPI Test Server"); + return retVal; + } + +} +//@formatter:on 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 new file mode 100644 index 00000000000..ecdf2b779da --- /dev/null +++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java @@ -0,0 +1,169 @@ + +package ca.uhn.fhir.jpa.demo; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu1; +import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2; +import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu1; +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.model.dstu2.composite.MetaDt; +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; +import ca.uhn.fhir.rest.server.ETagSupportEnum; +import ca.uhn.fhir.rest.server.EncodingEnum; +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.Collection; +import java.util.List; + +public class JpaServerDemoDstu2 extends RestfulServer { + + private static final long serialVersionUID = 1L; + + private WebApplicationContext myAppCtx; + + @SuppressWarnings("unchecked") + @Override + protected void initialize() throws ServletException { + super.initialize(); + + /* + * We want to support FHIR DSTU2 format. This means that the server + * will use the DSTU2 bundle format and other DSTU2 encoding changes. + * + * If you want to use DSTU1 instead, change the following line, and change the 2 occurrences of dstu2 in web.xml to dstu1 + */ + FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU2; + setFhirContext(new FhirContext(fhirVersion)); + + // Get the spring context from the web container (it's declared in web.xml) + myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); + + /* + * The BaseJavaConfigDstu2.java class is a spring configuration + * file which is automatically generated as a part of hapi-fhir-jpaserver-base and + * contains bean definitions for a resource provider for each resource type + */ + String resourceProviderBeanName; + if (fhirVersion == FhirVersionEnum.DSTU1) { + resourceProviderBeanName = "myResourceProvidersDstu1"; + } else if (fhirVersion == FhirVersionEnum.DSTU2) { + resourceProviderBeanName = "myResourceProvidersDstu2"; + } else if (fhirVersion == FhirVersionEnum.DSTU3) { + resourceProviderBeanName = "myResourceProvidersDstu3"; + } else { + throw new IllegalStateException(); + } + List beans = myAppCtx.getBean(resourceProviderBeanName, List.class); + setResourceProviders(beans); + + /* + * The system provider implements non-resource-type methods, such as + * transaction, and global history. + */ + Object systemProvider; + if (fhirVersion == FhirVersionEnum.DSTU1) { + systemProvider = myAppCtx.getBean("mySystemProviderDstu1", JpaSystemProviderDstu1.class); + } else if (fhirVersion == FhirVersionEnum.DSTU2) { + systemProvider = myAppCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class); + } else if (fhirVersion == FhirVersionEnum.DSTU3) { + systemProvider = myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class); + } else { + throw new IllegalStateException(); + } + setPlainProviders(systemProvider); + + /* + * The conformance provider exports the supported resources, search parameters, etc for + * this server. The JPA version adds resource counts to the exported statement, so it + * is a nice addition. + */ + if (fhirVersion == FhirVersionEnum.DSTU1) { + IFhirSystemDao, MetaDt> systemDao = myAppCtx.getBean("mySystemDaoDstu1", IFhirSystemDao.class); + JpaConformanceProviderDstu1 confProvider = new JpaConformanceProviderDstu1(this, systemDao); + confProvider.setImplementationDescription("Example Server"); + setServerConformanceProvider(confProvider); + } else if (fhirVersion == FhirVersionEnum.DSTU2) { + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); + JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, + myAppCtx.getBean(DaoConfig.class)); + confProvider.setImplementationDescription("Example Server"); + setServerConformanceProvider(confProvider); + } else if (fhirVersion == FhirVersionEnum.DSTU3) { + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); + JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, + myAppCtx.getBean(DaoConfig.class)); + confProvider.setImplementationDescription("Example Server"); + setServerConformanceProvider(confProvider); + } else { + throw new IllegalStateException(); + } + + /* + * Enable ETag Support (this is already the default) + */ + setETagSupport(ETagSupportEnum.ENABLED); + + /* + * This server tries to dynamically generate narratives + */ + FhirContext ctx = getFhirContext(); + ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + + /* + * Default to JSON and pretty printing + */ + setDefaultPrettyPrint(true); + setDefaultResponseEncoding(EncodingEnum.JSON); + + /* + * -- New in HAPI FHIR 1.5 -- + * This configures the server to page search results to and from + * the database, instead of only paging them to memory. This may mean + * a performance hit when performing searches that return lots of results, + * but makes the server much more scalable. + */ + setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + + /* + * 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); + } + + /* + * If you are hosting this server at a specific DNS name, the server will try to + * figure out the FHIR base URL based on what the web container tells it, but + * this doesn't always work. If you are setting links in your search bundles that + * just refer to "localhost", you might want to use a server address strategy: + */ + //setServerAddressStrategy(new HardcodedServerAddressStrategy("http://mydomain.com/fhir/baseDstu2")); + + /* + * If you are using DSTU3+, you may want to add a terminology uploader, which allows + * uploading of external terminologies such as Snomed CT. Note that this uploader + * does not have any security attached (any anonymous user may use it by default) + * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor + * with this feature. + */ + //if (fhirVersion == FhirVersionEnum.DSTU3) { + // registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); + //} + } + +} diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu2.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-dstu2.xml similarity index 94% rename from hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu2.xml rename to hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-dstu2.xml index 2bd24f980a9..de1a573d1c4 100644 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu2.xml +++ b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-dstu2.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ./xsd/web-app_3_0.xsd"> org.springframework.web.context.ContextLoaderListener @@ -13,7 +13,7 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirServerConfigWSocket + ca.uhn.fhir.jpa.demo.FhirServerConfigDstu2 @@ -28,7 +28,7 @@ contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirTesterConfig + ca.uhn.fhir.jpa.demo.FhirTesterConfigDstu2 2 @@ -103,6 +103,4 @@ CORS Filter /* - - diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml deleted file mode 100644 index e74f0193383..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-non-polling-subscription-dstu3.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - org.springframework.web.context.ContextLoaderListener - - - contextClass - - org.springframework.web.context.support.AnnotationConfigWebApplicationContext - - - - contextConfigLocation - - ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3WSocket - - - - - - - spring - org.springframework.web.servlet.DispatcherServlet - - contextClass - org.springframework.web.context.support.AnnotationConfigWebApplicationContext - - - contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirTesterConfig - - 2 - - - - fhirServlet - ca.uhn.fhir.jpa.demo.JpaServerDemo - - ImplementationDescription - FHIR JPA Server - - - FhirVersion - DSTU3 - - 1 - - - - fhirServlet - /baseDstu3/* - - - - spring - / - - - - - - - CORS Filter - org.ebaysf.web.cors.CORSFilter - - A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. - cors.allowed.origins - * - - - A comma separated list of HTTP verbs, using which a CORS request can be made. - cors.allowed.methods - GET,POST,PUT,DELETE,OPTIONS - - - A comma separated list of allowed headers when making a non simple CORS request. - cors.allowed.headers - X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers - - - A comma separated list non-standard response headers that will be exposed to XHR2 object. - cors.exposed.headers - Location,Content-Location - - - A flag that suggests if CORS is supported with cookies - cors.support.credentials - true - - - A flag to control logging - cors.logging.enabled - true - - - Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. - cors.preflight.maxage - 300 - - - - CORS Filter - /* - - - - diff --git a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml b/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml deleted file mode 100644 index c81a6f602d0..00000000000 --- a/hapi-fhir-jpaserver-example/src/main/webapp/WEB-INF/web-polling-subscription-dstu2.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - org.springframework.web.context.ContextLoaderListener - - - contextClass - - org.springframework.web.context.support.AnnotationConfigWebApplicationContext - - - - contextConfigLocation - - ca.uhn.fhir.jpa.demo.FhirServerConfig - - - - - - - spring - org.springframework.web.servlet.DispatcherServlet - - contextClass - org.springframework.web.context.support.AnnotationConfigWebApplicationContext - - - contextConfigLocation - ca.uhn.fhir.jpa.demo.FhirTesterConfig - - 2 - - - - fhirServlet - ca.uhn.fhir.jpa.demo.JpaServerDemo - - ImplementationDescription - FHIR JPA Server - - - FhirVersion - DSTU2 - - 1 - - - - fhirServlet - /baseDstu2/* - - - - spring - / - - - - - - - CORS Filter - org.ebaysf.web.cors.CORSFilter - - A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. - cors.allowed.origins - * - - - A comma separated list of HTTP verbs, using which a CORS request can be made. - cors.allowed.methods - GET,POST,PUT,DELETE,OPTIONS - - - A comma separated list of allowed headers when making a non simple CORS request. - cors.allowed.headers - X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers - - - A comma separated list non-standard response headers that will be exposed to XHR2 object. - cors.exposed.headers - Location,Content-Location - - - A flag that suggests if CORS is supported with cookies - cors.support.credentials - true - - - A flag to control logging - cors.logging.enabled - true - - - Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. - cors.preflight.maxage - 300 - - - - CORS Filter - /* - - - - From 70a396f703d827328b5be9c35fa0ea9417e1fe2f Mon Sep 17 00:00:00 2001 From: James Date: Sun, 25 Jun 2017 10:01:05 -0400 Subject: [PATCH 2/2] Credit for #678 --- src/changes/changes.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2adefcf2365..d251910d28c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -52,6 +52,11 @@ Prevent duplicates in $everything query response in JPA server. Thanks to @vlad-ignatov for reporting! + + FIx issue in SubscriptionInterceptor that caused interceptor to only + actually notify listeners of the first 10 subscriptions. Thanks to Jeff Chung + for the pull request! +