From 7f029475acac5007998641b82cf2d042eca8611d Mon Sep 17 00:00:00 2001 From: JP Date: Fri, 14 Oct 2022 09:40:03 -0600 Subject: [PATCH] Add MSSql Support, fix multiple versions dialects of Postgres being used (#637) * Add MSSql Support, fix multiple versions dialects of Postgres being used * Fix JPA Hibernate Properties provider --- .vscode/launch.json | 73 +---- .vscode/settings.json | 5 + .../ruler/config/FhirServerConfigCommon.java | 262 ++++++++++++++++++ .../opencds/cqf/ruler/config/RulerConfig.java | 1 - ...PatchedJpaHibernatePropertiesProvider.java | 47 ++++ docker-compose.yml | 23 ++ server/pom.xml | 8 +- server/src/main/resources/application.yaml | 2 + 8 files changed, 351 insertions(+), 70 deletions(-) create mode 100644 core/src/main/java/org/opencds/cqf/ruler/config/FhirServerConfigCommon.java create mode 100644 core/src/main/java/org/opencds/cqf/ruler/provider/PatchedJpaHibernatePropertiesProvider.java create mode 100644 docker-compose.yml diff --git a/.vscode/launch.json b/.vscode/launch.json index 48f8054dd..2f4083202 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,17 +17,6 @@ "-Dhapi.fhir.bulk_export_enabled=false", "-Dspring.batch.job.enabled=false" ], - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - } }, { "type": "java", @@ -42,17 +31,6 @@ "-Dhapi.fhir.bulk_export_enabled=false", "-Dspring.batch.job.enabled=false" ], - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - }, "classPaths": [ "$Auto", "$Runtime" @@ -65,25 +43,17 @@ "mainClass": "org.opencds.cqf.ruler.Application", "projectName": "cqf-ruler-server", "vmArgs": [ + "-XX:TieredStopAtLevel=1", + "-Ddebug=true", + "-Dloader.debug=true", "-Dspring.datasource.url=jdbc:postgresql://localhost:5432/postgres", "-Dspring.datasource.username=postgres", "-Dspring.datasource.password=", "-Dspring.datasource.driverClassName=org.postgresql.Driver", - "-Dspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect", + "-Dspring.jpa.properties.hibernate.dialect=ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect", "-Dhapi.fhir.bulk_export_enabled=false", "-Dspring.batch.job.enabled=false" ], - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - }, "classPaths": [ "$Auto", "$Runtime" @@ -103,17 +73,6 @@ "-Dhapi.fhir.bulk_export_enabled=false", "-Dspring.batch.job.enabled=false" ], - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - }, "classPaths": [ "$Auto", "$Runtime" @@ -124,35 +83,13 @@ "name": "Attach", "request": "attach", "hostName": "localhost", - "port": "5005", - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - } + "port": "5005" }, { "type": "java", "name": "Launch Current File", "request": "launch", "mainClass": "${file}", - "stepFilters": { - "classNameFilters": [ - "java.*", - "javax.*", - "com.sun.*", - "sun.*", - "sunw.*", - "org.omg.*", - "org.springframework.aop.*" - ] - } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index a6db07ef6..e2e415eda 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,10 @@ { "java.configuration.updateBuildConfiguration": "automatic", + "java.debug.settings.stepping.skipClasses": [ + "$JDK", + "java.lang.ClassLoader", + "org.springframework.aop.*" + ], "java.errors.incompleteClasspath.severity": "ignore", "java.test.config": [ { diff --git a/core/src/main/java/org/opencds/cqf/ruler/config/FhirServerConfigCommon.java b/core/src/main/java/org/opencds/cqf/ruler/config/FhirServerConfigCommon.java new file mode 100644 index 000000000..cf18bb61b --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/ruler/config/FhirServerConfigCommon.java @@ -0,0 +1,262 @@ +package org.opencds.cqf.ruler.config; + +import java.util.HashSet; +import java.util.Optional; + +import org.hl7.fhir.dstu2.model.Subscription; +import org.opencds.cqf.external.AppProperties; +import org.opencds.cqf.ruler.provider.PatchedJpaHibernatePropertiesProvider; +import org.springframework.boot.env.YamlPropertySourceLoader; +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.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.google.common.base.Strings; + +import ca.uhn.fhir.jpa.api.config.DaoConfig; +import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc; +import ca.uhn.fhir.jpa.binstore.DatabaseBlobBinaryStorageSvcImpl; +import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; +import ca.uhn.fhir.jpa.model.config.PartitionSettings; +import ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode; +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionDeliveryHandlerFactory; +import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl; +import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender; +import ca.uhn.fhir.rest.server.mail.IMailSvc; +import ca.uhn.fhir.rest.server.mail.MailConfig; +import ca.uhn.fhir.rest.server.mail.MailSvc; + +@Configuration +@EnableTransactionManagement +public class FhirServerConfigCommon { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirServerConfigCommon.class); + + public FhirServerConfigCommon(AppProperties appProperties) { + ourLog.info("Server configured to " + (appProperties.getAllow_contains_searches() ? "allow" : "deny") + + " contains searches"); + ourLog.info("Server configured to " + (appProperties.getAllow_multiple_delete() ? "allow" : "deny") + + " multiple deletes"); + ourLog.info("Server configured to " + (appProperties.getAllow_external_references() ? "allow" : "deny") + + " external references"); + ourLog.info("Server configured to " + (appProperties.getDao_scheduling_enabled() ? "enable" : "disable") + + " DAO scheduling"); + ourLog.info("Server configured to " + (appProperties.getDelete_expunge_enabled() ? "enable" : "disable") + + " delete expunges"); + ourLog.info("Server configured to " + (appProperties.getExpunge_enabled() ? "enable" : "disable") + " expunges"); + ourLog.info("Server configured to " + (appProperties.getAllow_override_default_search_params() ? "allow" : "deny") + + " overriding default search params"); + ourLog.info("Server configured to " + + (appProperties.getAuto_create_placeholder_reference_targets() ? "allow" : "disable") + + " auto-creating placeholder references"); + + if (appProperties.getSubscription().getEmail() != null) { + AppProperties.Subscription.Email email = appProperties.getSubscription().getEmail(); + ourLog.info( + "Server is configured to enable email with host '" + email.getHost() + "' and port " + email.getPort()); + ourLog.info("Server will use '" + email.getFrom() + "' as the from email address"); + + if (!Strings.isNullOrEmpty(email.getUsername())) { + ourLog.info("Server is configured to use username '" + email.getUsername() + "' for email"); + } + + if (!Strings.isNullOrEmpty(email.getPassword())) { + ourLog.info("Server is configured to use a password for email"); + } + } + + if (appProperties.getSubscription().getResthook_enabled()) { + ourLog.info("REST-hook subscriptions enabled"); + } + + if (appProperties.getSubscription().getEmail() != null) { + ourLog.info("Email subscriptions enabled"); + } + + if (appProperties.getEnable_index_contained_resource() == Boolean.TRUE) { + ourLog.info("Indexed on contained resource enabled"); + } + } + + /** + * Configure FHIR properties around the the JPA server via this bean + */ + @Bean + public DaoConfig daoConfig(AppProperties appProperties) { + DaoConfig retVal = new DaoConfig(); + + retVal.setIndexMissingFields(appProperties.getEnable_index_missing_fields() ? DaoConfig.IndexEnabledEnum.ENABLED + : DaoConfig.IndexEnabledEnum.DISABLED); + retVal.setAutoCreatePlaceholderReferenceTargets(appProperties.getAuto_create_placeholder_reference_targets()); + retVal.setEnforceReferentialIntegrityOnWrite(appProperties.getEnforce_referential_integrity_on_write()); + retVal.setEnforceReferentialIntegrityOnDelete(appProperties.getEnforce_referential_integrity_on_delete()); + retVal.setAllowContainsSearches(appProperties.getAllow_contains_searches()); + retVal.setAllowMultipleDelete(appProperties.getAllow_multiple_delete()); + retVal.setAllowExternalReferences(appProperties.getAllow_external_references()); + retVal.setSchedulingDisabled(!appProperties.getDao_scheduling_enabled()); + retVal.setDeleteExpungeEnabled(appProperties.getDelete_expunge_enabled()); + retVal.setExpungeEnabled(appProperties.getExpunge_enabled()); + if (appProperties.getSubscription() != null && appProperties.getSubscription().getEmail() != null) + retVal.setEmailFromAddress(appProperties.getSubscription().getEmail().getFrom()); + + Integer maxFetchSize = appProperties.getMax_page_size(); + retVal.setFetchSizeDefaultMaximum(maxFetchSize); + ourLog.info("Server configured to have a maximum fetch size of " + + (maxFetchSize == Integer.MAX_VALUE ? "'unlimited'" : maxFetchSize)); + + Long reuseCachedSearchResultsMillis = appProperties.getReuse_cached_search_results_millis(); + retVal.setReuseCachedSearchResultsForMillis(reuseCachedSearchResultsMillis); + ourLog.info("Server configured to cache search results for {} milliseconds", reuseCachedSearchResultsMillis); + + Long retainCachedSearchesMinutes = appProperties.getRetain_cached_searches_mins(); + retVal.setExpireSearchResultsAfterMillis(retainCachedSearchesMinutes * 60 * 1000); + + if (appProperties.getSubscription() != null) { + // Subscriptions are enabled by channel type + if (appProperties.getSubscription().getResthook_enabled()) { + ourLog.info("Enabling REST-hook subscriptions"); + retVal.addSupportedSubscriptionType(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.RESTHOOK); + } + if (appProperties.getSubscription().getEmail() != null) { + ourLog.info("Enabling email subscriptions"); + retVal.addSupportedSubscriptionType(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.EMAIL); + } + if (appProperties.getSubscription().getWebsocket_enabled()) { + ourLog.info("Enabling websocket subscriptions"); + retVal.addSupportedSubscriptionType( + org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.WEBSOCKET); + } + } + + retVal.setFilterParameterEnabled(appProperties.getFilter_search_enabled()); + retVal.setAdvancedHSearchIndexing(appProperties.getAdvanced_lucene_indexing()); + retVal.setTreatBaseUrlsAsLocal(new HashSet<>(appProperties.getLocal_base_urls())); + + return retVal; + } + + @Bean + public YamlPropertySourceLoader yamlPropertySourceLoader() { + return new YamlPropertySourceLoader(); + } + + @Bean + public PartitionSettings partitionSettings(AppProperties appProperties) { + PartitionSettings retVal = new PartitionSettings(); + + // Partitioning + if (appProperties.getPartitioning() != null) { + retVal.setPartitioningEnabled(true); + retVal.setIncludePartitionInSearchHashes( + appProperties.getPartitioning().getPartitioning_include_in_search_hashes()); + if (appProperties.getPartitioning().getAllow_references_across_partitions()) { + retVal.setAllowReferencesAcrossPartitions(CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED); + } else { + retVal.setAllowReferencesAcrossPartitions(CrossPartitionReferenceMode.NOT_ALLOWED); + } + } + + return retVal; + } + + @Primary + @Bean + public HibernatePropertiesProvider jpaStarterDialectProvider( + LocalContainerEntityManagerFactoryBean myEntityManagerFactory) { + return new PatchedJpaHibernatePropertiesProvider(myEntityManagerFactory); + } + + @Bean + public ModelConfig modelConfig(AppProperties appProperties, DaoConfig daoConfig) { + ModelConfig modelConfig = daoConfig.getModelConfig(); + modelConfig.setAllowContainsSearches(appProperties.getAllow_contains_searches()); + modelConfig.setAllowExternalReferences(appProperties.getAllow_external_references()); + modelConfig.setDefaultSearchParamsCanBeOverridden(appProperties.getAllow_override_default_search_params()); + if (appProperties.getSubscription() != null && appProperties.getSubscription().getEmail() != null) + modelConfig.setEmailFromAddress(appProperties.getSubscription().getEmail().getFrom()); + + // You can enable these if you want to support Subscriptions from your server + if (appProperties.getSubscription() != null && appProperties.getSubscription().getResthook_enabled() != null) { + modelConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.RESTHOOK); + } + + if (appProperties.getSubscription() != null && appProperties.getSubscription().getEmail() != null) { + modelConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.EMAIL); + } + + modelConfig.setNormalizedQuantitySearchLevel(appProperties.getNormalized_quantity_search_level()); + + modelConfig.setIndexOnContainedResources(appProperties.getEnable_index_contained_resource()); + modelConfig.setIndexIdentifierOfType(appProperties.getEnable_index_of_type()); + return modelConfig; + } + + /** + * 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 BasicDataSource dataSource() throws ClassNotFoundException, + * NoSuchMethodException, IllegalAccessException, InvocationTargetException, + * InstantiationException { + * BasicDataSource retVal = new BasicDataSource(); + * Driver driver = (Driver) + * Class.forName(HapiProperties.getDataSourceDriver()).getConstructor(). + * newInstance(); + * retVal.setDriver(driver); + * retVal.setUrl(HapiProperties.getDataSourceUrl()); + * retVal.setUsername(HapiProperties.getDataSourceUsername()); + * retVal.setPassword(HapiProperties.getDataSourcePassword()); + * retVal.setMaxTotal(HapiProperties.getDataSourceMaxPoolSize()); + * return retVal; + * } + */ + + @Lazy + @Bean + public IBinaryStorageSvc binaryStorageSvc(AppProperties appProperties) { + DatabaseBlobBinaryStorageSvcImpl binaryStorageSvc = new DatabaseBlobBinaryStorageSvcImpl(); + + if (appProperties.getMax_binary_size() != null) { + binaryStorageSvc.setMaximumBinarySize(appProperties.getMax_binary_size()); + } + + return binaryStorageSvc; + } + + @Bean + public IEmailSender emailSender(AppProperties appProperties, + Optional subscriptionDeliveryHandlerFactory) { + if (appProperties.getSubscription() != null && appProperties.getSubscription().getEmail() != null) { + MailConfig mailConfig = new MailConfig(); + + AppProperties.Subscription.Email email = appProperties.getSubscription().getEmail(); + mailConfig.setSmtpHostname(email.getHost()); + mailConfig.setSmtpPort(email.getPort()); + mailConfig.setSmtpUsername(email.getUsername()); + mailConfig.setSmtpPassword(email.getPassword()); + mailConfig.setSmtpUseStartTLS(email.getStartTlsEnable()); + + IMailSvc mailSvc = new MailSvc(mailConfig); + IEmailSender emailSender = new EmailSenderImpl(mailSvc); + + subscriptionDeliveryHandlerFactory + .ifPresent(theSubscriptionDeliveryHandlerFactory -> theSubscriptionDeliveryHandlerFactory + .setEmailSender(emailSender)); + + return emailSender; + } + + return null; + } +} diff --git a/core/src/main/java/org/opencds/cqf/ruler/config/RulerConfig.java b/core/src/main/java/org/opencds/cqf/ruler/config/RulerConfig.java index cf1b82d49..c1954d262 100644 --- a/core/src/main/java/org/opencds/cqf/ruler/config/RulerConfig.java +++ b/core/src/main/java/org/opencds/cqf/ruler/config/RulerConfig.java @@ -1,7 +1,6 @@ package org.opencds.cqf.ruler.config; import org.opencds.cqf.external.AppProperties; -import org.opencds.cqf.external.FhirServerConfigCommon; import org.springframework.context.annotation.Import; import ca.uhn.fhir.batch2.jobs.config.Batch2JobsConfig; diff --git a/core/src/main/java/org/opencds/cqf/ruler/provider/PatchedJpaHibernatePropertiesProvider.java b/core/src/main/java/org/opencds/cqf/ruler/provider/PatchedJpaHibernatePropertiesProvider.java new file mode 100644 index 000000000..da1039245 --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/ruler/provider/PatchedJpaHibernatePropertiesProvider.java @@ -0,0 +1,47 @@ +package org.opencds.cqf.ruler.provider; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.internal.StandardDialectResolver; +import org.hibernate.engine.jdbc.dialect.spi.DatabaseMetaDataDialectResolutionInfoAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; +import ca.uhn.fhir.util.ReflectionUtil; + +public class PatchedJpaHibernatePropertiesProvider extends HibernatePropertiesProvider { + + private LocalContainerEntityManagerFactoryBean entityManagerFactory; + + private Dialect dialect; + + public PatchedJpaHibernatePropertiesProvider(LocalContainerEntityManagerFactoryBean entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @Override + public Dialect getDialect() { + if (dialect == null) { + String dialectClass = (String) entityManagerFactory.getJpaPropertyMap().get("hibernate.dialect"); + dialect = ReflectionUtil.newInstanceOrReturnNull(dialectClass, Dialect.class); + } + + if (dialect == null) { + DataSource connection = entityManagerFactory.getDataSource(); + try (Connection c = connection.getConnection()) { + dialect = new StandardDialectResolver() + .resolveDialect( + new DatabaseMetaDataDialectResolutionInfoAdapter(c.getMetaData())); + } catch (SQLException sqlException) { + throw new ConfigurationException(sqlException.getMessage(), sqlException); + } + } + + return dialect; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..48e72dc09 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + # dqm: + # depends_on: + # - postgres + # image: alphora/cqf-ruler:testy + # ports: + # - 8080:8080 + # environment: + # - "spring.datasource.url=jdbc:postgresql://postgres:5432/postgres" + # - "spring.datasource.username=postgres" + # - "spring.datasource.password=" + # - "spring.datasource.driverClassName=org.postgresql.Driver" + # - "spring.jpa.properties.hibernate.dialect=ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect" + # - "spring.jpa.database-platform=ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect" + # - "hapi.fhir.bulk_export_enabled=false" + # - "spring.batch.job.enabled=false" + + postgres: + image: postgres + ports: + - 5432:5432 + environment: + - "POSTGRES_HOST_AUTH_METHOD=trust" diff --git a/server/pom.xml b/server/pom.xml index 5e7d1c4c7..f1d9d13f6 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.opencds.cqf.ruler @@ -30,6 +31,11 @@ postgresql + + com.microsoft.sqlserver + mssql-jdbc + + commons-io commons-io diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 4f45a61b3..662d3bcde 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -40,10 +40,12 @@ hapi: ## RA Settings ra: + #e enabled: true report: endpoint: "" sdc: + # enabled: true extract: endpoint: "http://localhost:8080/fhir/" username: