diff --git a/hapi-fhir-jpaserver-cds-example/pom.xml b/hapi-fhir-jpaserver-cds-example/pom.xml new file mode 100644 index 00000000000..4059173b5f4 --- /dev/null +++ b/hapi-fhir-jpaserver-cds-example/pom.xml @@ -0,0 +1,286 @@ + + 4.0.0 + + + + ca.uhn.hapi.fhir + hapi-fhir + 3.1.0-SNAPSHOT + ../pom.xml + + + hapi-fhir-jpaserver-cds + war + + HAPI FHIR JPA Clinical Decision Support Server - Example + + + + oss-snapshots + + true + + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + + + org.opencds.cqf + cqf-ruler + 0.1.0-SNAPSHOT + + + + org.eclipse.jetty.websocket + websocket-api + ${jetty_version} + + + org.eclipse.jetty.websocket + websocket-client + ${jetty_version} + + + mysql + mysql-connector-java + 6.0.5 + + + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${project.version} + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu3 + ${project.version} + + + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-base + ${project.version} + + + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + ${project.version} + war + provided + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + ${project.version} + classes + provided + + + + + ch.qos.logback + logback-classic + + + + + javax.servlet + javax.servlet-api + provided + + + + + org.thymeleaf + thymeleaf + + + + + org.ebaysf.web + cors-filter + + + servlet-api + javax.servlet + + + + + + + org.springframework + spring-web + + + + + org.apache.commons + commons-dbcp2 + + + + + org.apache.derby + derby + + + org.apache.derby + derbynet + + + org.apache.derby + derbyclient + + + + + org.eclipse.jetty + jetty-servlets + test + + + org.eclipse.jetty + jetty-servlet + test + + + org.eclipse.jetty.websocket + websocket-server + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-util + test + + + org.eclipse.jetty + jetty-webapp + test + + + com.phloc + phloc-schematron + + + Saxon-HE + net.sf.saxon + + + + + + + javax.interceptor + javax.interceptor-api + provided + + + + + + + + hapi-fhir-jpaserver-cds + + + + + + org.eclipse.jetty + jetty-maven-plugin + + + /hapi-fhir-jpaserver-cds + true + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + ${maven.build.timestamp} + + + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + + + src/main/webapp/WEB-INF/web.xml + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + integration-test + verify + + + + + + + + + diff --git a/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsHooksServerExample.java b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsHooksServerExample.java new file mode 100644 index 00000000000..e17bc747437 --- /dev/null +++ b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsHooksServerExample.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.jpa.cds.example; + +import org.opencds.cqf.servlet.CdsServicesServlet; + +public class CdsHooksServerExample extends CdsServicesServlet { + +// @Override +// protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +// // Change how requests are handled +// } +// +// @Override +// protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { +// // Change discovery response +// } +} diff --git a/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsServerExample.java b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsServerExample.java new file mode 100644 index 00000000000..ed5e7185cd4 --- /dev/null +++ b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/CdsServerExample.java @@ -0,0 +1,165 @@ + +package ca.uhn.fhir.jpa.cds.example; + +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.dstu3.JpaConformanceProviderDstu3; +import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; +import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; +import ca.uhn.fhir.jpa.rp.dstu3.ActivityDefinitionResourceProvider; +import ca.uhn.fhir.jpa.rp.dstu3.MeasureResourceProvider; +import ca.uhn.fhir.jpa.rp.dstu3.PlanDefinitionResourceProvider; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +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.opencds.cqf.providers.FHIRActivityDefinitionResourceProvider; +import org.opencds.cqf.providers.FHIRMeasureResourceProvider; +import org.opencds.cqf.providers.FHIRPlanDefinitionResourceProvider; +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 CdsServerExample extends RestfulServer { + + @SuppressWarnings("unchecked") + @Override + protected void initialize() throws ServletException { + super.initialize(); + + FhirVersionEnum fhirVersion = FhirVersionEnum.DSTU3; + setFhirContext(new FhirContext(fhirVersion)); + + // Get the spring context from the web container (it's declared in web.xml) + WebApplicationContext myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); + + if (myAppCtx == null) { + throw new ServletException("Error retrieving spring context from the web container"); + } + + String resourceProviderBeanName = "myResourceProvidersDstu3"; + List beans = myAppCtx.getBean(resourceProviderBeanName, List.class); + setResourceProviders(beans); + + Object systemProvider = myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class); + 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. + */ + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); + JpaConformanceProviderDstu3 confProvider = + new JpaConformanceProviderDstu3(this, systemDao, myAppCtx.getBean(DaoConfig.class)); + confProvider.setImplementationDescription("Example Server"); + setServerConformanceProvider(confProvider); + + /* + * 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); + } + + /* + * Adding resource providers from the cqf-ruler + */ + // Measure processing + FHIRMeasureResourceProvider measureProvider = new FHIRMeasureResourceProvider(getResourceProviders()); + MeasureResourceProvider jpaMeasureProvider = (MeasureResourceProvider) getProvider("Measure"); + measureProvider.setDao(jpaMeasureProvider.getDao()); + measureProvider.setContext(jpaMeasureProvider.getContext()); + + // PlanDefinition processing + FHIRPlanDefinitionResourceProvider planDefProvider = new FHIRPlanDefinitionResourceProvider(getResourceProviders()); + PlanDefinitionResourceProvider jpaPlanDefProvider = + (PlanDefinitionResourceProvider) getProvider("PlanDefinition"); + planDefProvider.setDao(jpaPlanDefProvider.getDao()); + planDefProvider.setContext(jpaPlanDefProvider.getContext()); + + // ActivityDefinition processing + FHIRActivityDefinitionResourceProvider actDefProvider = new FHIRActivityDefinitionResourceProvider(getResourceProviders()); + ActivityDefinitionResourceProvider jpaActDefProvider = + (ActivityDefinitionResourceProvider) getProvider("ActivityDefinition"); + actDefProvider.setDao(jpaActDefProvider.getDao()); + actDefProvider.setContext(jpaActDefProvider.getContext()); + + try { + unregisterProvider(jpaMeasureProvider); + unregisterProvider(jpaPlanDefProvider); + unregisterProvider(jpaActDefProvider); + } catch (Exception e) { + throw new ServletException("Unable to unregister provider: " + e.getMessage()); + } + + registerProvider(measureProvider); + registerProvider(planDefProvider); + registerProvider(actDefProvider); + + /* + * 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. + */ + registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); + } + + public IResourceProvider getProvider(String name) { + + for (IResourceProvider res : getResourceProviders()) { + if (res.getResourceType().getSimpleName().equals(name)) { + return res; + } + } + + throw new IllegalArgumentException("This should never happen!"); + } +} diff --git a/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirServerConfig.java b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirServerConfig.java new file mode 100644 index 00000000000..58a9536b281 --- /dev/null +++ b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirServerConfig.java @@ -0,0 +1,125 @@ +package ca.uhn.fhir.jpa.cds.example; + +import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory; +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 FhirServerConfig extends BaseJavaConfigDstu3 { + + /** + * 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.model_mapping", LuceneSearchMappingFactory.class.getName()); + 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() { + SubscriptionsRequireManualActivationInterceptorDstu3 retVal = new SubscriptionsRequireManualActivationInterceptorDstu3(); + return retVal; + } + + @Bean() + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager retVal = new JpaTransactionManager(); + retVal.setEntityManagerFactory(entityManagerFactory); + return retVal; + } + +} diff --git a/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirTesterConfig.java b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirTesterConfig.java new file mode 100644 index 00000000000..6fbc660b27d --- /dev/null +++ b/hapi-fhir-jpaserver-cds-example/src/main/java/ca/uhn/fhir/jpa/cds/example/FhirTesterConfig.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.jpa.cds.example; + +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 FhirTesterConfig { + + /** + * 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.DSTU3) + .withBaseUrl("${serverBase}/baseDstu3") + .withName("Local Tester") + .addServer() + .withId("hapi") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://fhirtest.uhn.ca/baseDstu3") + .withName("Public HAPI Test Server"); + return retVal; + } + +} +//@formatter:on diff --git a/hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/about.html b/hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/about.html similarity index 97% rename from hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/about.html rename to hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/about.html index 33033992b9f..d552027e956 100644 --- a/hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/about.html +++ b/hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/about.html @@ -1,5 +1,5 @@ - + About This Server diff --git a/hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/tmpl-footer.html b/hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/tmpl-footer.html similarity index 90% rename from hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/tmpl-footer.html rename to hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/tmpl-footer.html index 5b87f43f1eb..bf18c498a78 100644 --- a/hapi-fhir-jpaserver-cqf-ruler/src/main/webapp/WEB-INF/templates/tmpl-footer.html +++ b/hapi-fhir-jpaserver-cds-example/src/main/webapp/WEB-INF/templates/tmpl-footer.html @@ -1,5 +1,5 @@ - +