diff --git a/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/DatasourceConfiguration.java b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/DatasourceConfiguration.java index 80cb0839a..a4d2b8c90 100644 --- a/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/DatasourceConfiguration.java +++ b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/DatasourceConfiguration.java @@ -31,6 +31,8 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; + +import java.util.HashMap; import java.util.Map; /** @@ -52,6 +54,7 @@ public class DatasourceConfiguration extends BasicDataSource implements BasicJdb private static final Logger LOG = LoggerFactory.getLogger(DatasourceConfiguration.class); private final CalculatedSettings calculatedSettings; private final String name; + private Map individualDsProperties = new HashMap<>(); /** * Constructor. @@ -165,6 +168,7 @@ public void setConnectionPropertiesString(@Property(name = "datasources.*.connec @Override public void setDataSourceProperties(@MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW) Map dsProperties) { + this.individualDsProperties = dsProperties; if (dsProperties != null) { dsProperties.forEach((s, o) -> { if (o != null) { @@ -194,4 +198,8 @@ void setEnabled(boolean enabled) { throw new DisabledBeanException("The datasource \"" + name + "\" is disabled"); } } + + Map getIndividualDsProperties() { + return individualDsProperties; + } } diff --git a/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceConfigModifier.java b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceConfigModifier.java new file mode 100644 index 000000000..6c5254485 --- /dev/null +++ b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceConfigModifier.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.dbcp; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +/** + * A bean created event listener that applies global datasource properties to + * {@link DatasourceConfiguration} beans when they are created. This modifier + * ensures that global properties defined under the "global.datasources.data-source-properties" + * configuration prefix are automatically applied to all datasource configurations, + * while preserving individual datasource-specific settings that take precedence. + * + *

The modifier only adds global properties that are not already present in the + * individual datasource configuration, ensuring that specific configurations always + * override global defaults. Properties with null values are ignored.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@Singleton +public class GlobalDatasourceConfigModifier implements BeanCreatedEventListener { + + @Inject + GlobalDatasourceProperties globalDatasourceProperties; + + @Override + public DatasourceConfiguration onCreated(BeanCreatedEvent event) { + + DatasourceConfiguration configuration = event.getBean(); + globalDatasourceProperties.getDataSourceProperties() + .forEach((key, value) -> { + if (value != null && !configuration.getIndividualDsProperties().containsKey(key)) { + configuration.addConnectionProperty(key, value); + } + } + ); + return configuration; + } +} diff --git a/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceProperties.java b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceProperties.java new file mode 100644 index 000000000..f0316316d --- /dev/null +++ b/jdbc-dbcp/src/main/java/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourceProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.dbcp; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.naming.conventions.StringConvention; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for global datasource settings that can be applied + * to all {@link DatasourceConfiguration} instances. This class binds configuration + * properties under the "global.datasources" prefix and provides a way to define + * common datasource properties that should be applied across all datasources + * in the application. + * + *

Properties defined here serve as defaults that can be overridden by + * individual datasource configurations. The primary use case is to avoid + * repetition when multiple datasources share common settings such as + * connection pool parameters, SSL settings, or application-specific properties.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present, ensuring it doesn't interfere with applications + * that don't use global datasource configuration.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@ConfigurationProperties("global.datasources") +public class GlobalDatasourceProperties { + private Map dataSourceProperties = new HashMap<>(); + + public Map getDataSourceProperties() { + return dataSourceProperties; + } + + public void setDataSourceProperties(@MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW) Map dataSourceProperties) { + this.dataSourceProperties.putAll(dataSourceProperties); + } +} diff --git a/jdbc-dbcp/src/test/groovy/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourcePropertiesSpec.groovy b/jdbc-dbcp/src/test/groovy/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourcePropertiesSpec.groovy new file mode 100644 index 000000000..991a09e03 --- /dev/null +++ b/jdbc-dbcp/src/test/groovy/io/micronaut/configuration/jdbc/dbcp/GlobalDatasourcePropertiesSpec.groovy @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +//file:noinspection GroovyAccessibility +package io.micronaut.configuration.jdbc.dbcp + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.MapPropertySource +import spock.lang.Specification + +class GlobalDatasourcePropertiesSpec extends Specification { + + void "test no global datasource configuration exists when no global properties are present"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default'] + )) + applicationContext.start() + + when: + Optional properties = applicationContext.findBean(GlobalDatasourceProperties) + Optional datasourceConfig = applicationContext.findBean(DatasourceConfiguration) + + then: "No global beans are created when no global configuration is present" + datasourceConfig.isPresent() + datasourceConfig.get().connectionProperties.isEmpty() + datasourceConfig.get().url == 'jdbc:h2:mem:default' + properties.isEmpty() + + cleanup: + applicationContext.close() + } + + void "test global datasource properties configuration creates correct beans"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['global.datasources.data-source-properties.ApplicationName': 'MyApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.reWriteBatchInserts': true] + )) + applicationContext.start() + + when: + GlobalDatasourceProperties properties = applicationContext.getBean(GlobalDatasourceProperties) + + then: "GlobalDatasourceProperties bean is created with correct properties" + properties != null + properties.dataSourceProperties != null + properties.dataSourceProperties.size() == 3 + properties.dataSourceProperties['ApplicationName'] == 'MyApp' + properties.dataSourceProperties['assumeMinServerVersion'] == '9.0' + properties.dataSourceProperties['reWriteBatchInserts'] == "true" + + and: "GlobalDatasourceConfigModifier bean is also created" + applicationContext.containsBean(GlobalDatasourceConfigModifier) + + cleanup: + applicationContext.close() + } + + void "test global properties are applied to all datasource configurations"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.secondary.url': 'jdbc:h2:mem:secondary', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0'] + )) + applicationContext.start() + + when: + def datasourceConfigs = applicationContext.getBeansOfType(DatasourceConfiguration) + + then: "Global properties are applied to all DatasourceConfiguration beans" + datasourceConfigs.size() == 2 + datasourceConfigs[0].connectionProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[0].connectionProperties['assumeMinServerVersion'] == '9.0' + datasourceConfigs[1].connectionProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[1].connectionProperties['assumeMinServerVersion'] == '9.0' + + cleanup: + applicationContext.close() + } + + void "test individual datasource properties override global properties"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.default.data-source-properties.ApplicationName': 'SpecificApp', + 'datasources.default.data-source-properties.specificProperty': 'specificValue', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.globalProperty': 'globalValue'] + )) + applicationContext.start() + + when: + DatasourceConfiguration datasourceConfig = applicationContext.getBean(DatasourceConfiguration) + + then: "Individual properties override globals, but global properties are still added" + datasourceConfig != null + datasourceConfig.connectionProperties['ApplicationName'] == 'SpecificApp' // Overridden + datasourceConfig.connectionProperties['specificProperty'] == 'specificValue' // Individual property + datasourceConfig.connectionProperties['assumeMinServerVersion'] == '9.0' // Added from global + datasourceConfig.connectionProperties['globalProperty'] == 'globalValue' // Added from global + + cleanup: + applicationContext.close() + } +} diff --git a/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceConfigModifier.java b/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceConfigModifier.java new file mode 100644 index 000000000..b9f87b901 --- /dev/null +++ b/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceConfigModifier.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.hikari; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +/** + * A bean created event listener that applies global datasource properties to + * {@link DatasourceConfiguration} beans when they are created. This modifier + * ensures that global properties defined under the "global.datasources.data-source-properties" + * configuration prefix are automatically applied to all datasource configurations, + * while preserving individual datasource-specific settings that take precedence. + * + *

The modifier only adds global properties that are not already present in the + * individual datasource configuration, ensuring that specific configurations always + * override global defaults. Properties with null values are ignored.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@Singleton +public class GlobalDatasourceConfigModifier implements BeanCreatedEventListener { + + @Inject + GlobalDatasourceProperties globalDatasourceProperties; + + @Override + public DatasourceConfiguration onCreated(BeanCreatedEvent event) { + + DatasourceConfiguration configuration = event.getBean(); + globalDatasourceProperties.getDataSourceProperties() + .forEach((key, value) -> { + if (value != null && !configuration.getDataSourceProperties().containsKey(key)) { + configuration.addDataSourceProperty(key, value); + } + } + ); + return configuration; + } +} diff --git a/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceProperties.java b/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceProperties.java new file mode 100644 index 000000000..58199f215 --- /dev/null +++ b/jdbc-hikari/src/main/java/io/micronaut/configuration/jdbc/hikari/GlobalDatasourceProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.hikari; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.naming.conventions.StringConvention; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for global datasource settings that can be applied + * to all {@link DatasourceConfiguration} instances. This class binds configuration + * properties under the "global.datasources" prefix and provides a way to define + * common datasource properties that should be applied across all datasources + * in the application. + * + *

Properties defined here serve as defaults that can be overridden by + * individual datasource configurations. The primary use case is to avoid + * repetition when multiple datasources share common settings such as + * connection pool parameters, SSL settings, or application-specific properties.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present, ensuring it doesn't interfere with applications + * that don't use global datasource configuration.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@ConfigurationProperties("global.datasources") +public class GlobalDatasourceProperties { + private Map dataSourceProperties = new HashMap<>(); + + public Map getDataSourceProperties() { + return dataSourceProperties; + } + + public void setDataSourceProperties(@MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW) Map dataSourceProperties) { + this.dataSourceProperties.putAll(dataSourceProperties); + } +} diff --git a/jdbc-hikari/src/test/groovy/io/micronaut/configuration/jdbc/hikari/GlobalDatasourcePropertiesSpec.groovy b/jdbc-hikari/src/test/groovy/io/micronaut/configuration/jdbc/hikari/GlobalDatasourcePropertiesSpec.groovy new file mode 100644 index 000000000..7af1a7095 --- /dev/null +++ b/jdbc-hikari/src/test/groovy/io/micronaut/configuration/jdbc/hikari/GlobalDatasourcePropertiesSpec.groovy @@ -0,0 +1,130 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.hikari + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.MapPropertySource +import spock.lang.Specification + +class GlobalDatasourcePropertiesSpec extends Specification { + + void "test no global datasource configuration exists when no global properties are present"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default'] + )) + applicationContext.start() + + when: + Optional properties = applicationContext.findBean(GlobalDatasourceProperties) + Optional datasourceConfig = applicationContext.findBean(DatasourceConfiguration) + + then: "No global beans are created when no global configuration is present" + datasourceConfig.isPresent() + datasourceConfig.get().dataSourceProperties.isEmpty() + datasourceConfig.get().url == 'jdbc:h2:mem:default' + properties.isEmpty() + + cleanup: + applicationContext.close() + } + + void "test global datasource properties configuration creates correct beans"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['global.datasources.data-source-properties.ApplicationName': 'MyApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.reWriteBatchInserts': true] + )) + applicationContext.start() + + when: + GlobalDatasourceProperties properties = applicationContext.getBean(GlobalDatasourceProperties) + + then: "GlobalDatasourceProperties bean is created with correct properties" + properties != null + properties.dataSourceProperties != null + properties.dataSourceProperties.size() == 3 + properties.dataSourceProperties['ApplicationName'] == 'MyApp' + properties.dataSourceProperties['assumeMinServerVersion'] == '9.0' + properties.dataSourceProperties['reWriteBatchInserts'] == true + + and: "GlobalDatasourceConfigModifier bean is also created" + applicationContext.containsBean(GlobalDatasourceConfigModifier) + + cleanup: + applicationContext.close() + } + + void "test global properties are applied to all datasource configurations"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.secondary.url': 'jdbc:h2:mem:secondary', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0'] + )) + applicationContext.start() + + when: + def datasourceConfigs = applicationContext.getBeansOfType(DatasourceConfiguration) + + then: "Global properties are applied to all DatasourceConfiguration beans" + datasourceConfigs.size() == 2 + datasourceConfigs[0].dataSourceProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[0].dataSourceProperties['assumeMinServerVersion'] == '9.0' + datasourceConfigs[1].dataSourceProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[1].dataSourceProperties['assumeMinServerVersion'] == '9.0' + + cleanup: + applicationContext.close() + } + + void "test individual datasource properties override global properties"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.default.data-source-properties.ApplicationName': 'SpecificApp', + 'datasources.default.data-source-properties.specificProperty': 'specificValue', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.globalProperty': 'globalValue'] + )) + applicationContext.start() + + when: + DatasourceConfiguration datasourceConfig = applicationContext.getBean(DatasourceConfiguration) + + then: "Individual properties override globals, but global properties are still added" + datasourceConfig != null + datasourceConfig.dataSourceProperties['ApplicationName'] == 'SpecificApp' // Overridden + datasourceConfig.dataSourceProperties['specificProperty'] == 'specificValue' // Individual property + datasourceConfig.dataSourceProperties['assumeMinServerVersion'] == '9.0' // Added from global + datasourceConfig.dataSourceProperties['globalProperty'] == 'globalValue' // Added from global + + cleanup: + applicationContext.close() + } +} diff --git a/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceConfigModifier.java b/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceConfigModifier.java new file mode 100644 index 000000000..425ed4eee --- /dev/null +++ b/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceConfigModifier.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.tomcat; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Properties; + +/** + * A bean created event listener that applies global datasource properties to + * {@link DatasourceConfiguration} beans when they are created. This modifier + * ensures that global properties defined under the "global.datasources.data-source-properties" + * configuration prefix are automatically applied to all datasource configurations, + * while preserving individual datasource-specific settings that take precedence. + * + *

The modifier only adds global properties that are not already present in the + * individual datasource configuration, ensuring that specific configurations always + * override global defaults. Properties with null values are ignored.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@Singleton +public class GlobalDatasourceConfigModifier implements BeanCreatedEventListener { + + @Inject + GlobalDatasourceProperties globalDatasourceProperties; + + @Override + public DatasourceConfiguration onCreated(BeanCreatedEvent event) { + + DatasourceConfiguration configuration = event.getBean(); + Properties existingProperties = configuration.getDbProperties(); + if (existingProperties == null) { + existingProperties = new Properties(); + } + + final Properties finalExistingProperties = existingProperties; + globalDatasourceProperties.getDataSourceProperties() + .forEach((key, value) -> { + if (value != null && !finalExistingProperties.containsKey(key)) { + finalExistingProperties.setProperty(key, value.toString()); + } + } + ); + + return configuration; + } +} diff --git a/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceProperties.java b/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceProperties.java new file mode 100644 index 000000000..1b4019756 --- /dev/null +++ b/jdbc-tomcat/src/main/java/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourceProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.tomcat; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.naming.conventions.StringConvention; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for global datasource settings that can be applied + * to all {@link DatasourceConfiguration} instances. This class binds configuration + * properties under the "global.datasources" prefix and provides a way to define + * common datasource properties that should be applied across all datasources + * in the application. + * + *

Properties defined here serve as defaults that can be overridden by + * individual datasource configurations. The primary use case is to avoid + * repetition when multiple datasources share common settings such as + * connection pool parameters, SSL settings, or application-specific properties.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present, ensuring it doesn't interfere with applications + * that don't use global datasource configuration.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@ConfigurationProperties("global.datasources") +public class GlobalDatasourceProperties { + private Map dataSourceProperties = new HashMap<>(); + + public Map getDataSourceProperties() { + return dataSourceProperties; + } + + public void setDataSourceProperties(@MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW) Map dataSourceProperties) { + this.dataSourceProperties.putAll(dataSourceProperties); + } +} diff --git a/jdbc-tomcat/src/test/groovy/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourcePropertiesSpec.groovy b/jdbc-tomcat/src/test/groovy/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourcePropertiesSpec.groovy new file mode 100644 index 000000000..5b8cf572a --- /dev/null +++ b/jdbc-tomcat/src/test/groovy/io/micronaut/configuration/jdbc/tomcat/GlobalDatasourcePropertiesSpec.groovy @@ -0,0 +1,130 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.tomcat + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.MapPropertySource +import spock.lang.Specification + +class GlobalDatasourcePropertiesSpec extends Specification { + + void "test no global datasource configuration exists when no global properties are present"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default'] + )) + applicationContext.start() + + when: + Optional properties = applicationContext.findBean(GlobalDatasourceProperties) + Optional datasourceConfig = applicationContext.findBean(DatasourceConfiguration) + + then: "No global beans are created when no global configuration is present" + datasourceConfig.isPresent() + datasourceConfig.get().dbProperties.isEmpty() + datasourceConfig.get().url == 'jdbc:h2:mem:default' + properties.isEmpty() + + cleanup: + applicationContext.close() + } + + void "test global datasource properties configuration creates correct beans"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['global.datasources.data-source-properties.ApplicationName': 'MyApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.reWriteBatchInserts': true] + )) + applicationContext.start() + + when: + GlobalDatasourceProperties properties = applicationContext.getBean(GlobalDatasourceProperties) + + then: "GlobalDatasourceProperties bean is created with correct properties" + properties != null + properties.dataSourceProperties != null + properties.dataSourceProperties.size() == 3 + properties.dataSourceProperties['ApplicationName'] == 'MyApp' + properties.dataSourceProperties['assumeMinServerVersion'] == '9.0' + properties.dataSourceProperties['reWriteBatchInserts'] == true + + and: "GlobalDatasourceConfigModifier bean is also created" + applicationContext.containsBean(GlobalDatasourceConfigModifier) + + cleanup: + applicationContext.close() + } + + void "test global properties are applied to all datasource configurations"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.secondary.url': 'jdbc:h2:mem:secondary', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0'] + )) + applicationContext.start() + + when: + def datasourceConfigs = applicationContext.getBeansOfType(DatasourceConfiguration) + + then: "Global properties are applied to all DatasourceConfiguration beans" + datasourceConfigs.size() == 2 + datasourceConfigs[0].dbProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[0].dbProperties['assumeMinServerVersion'] == '9.0' + datasourceConfigs[1].dbProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[1].dbProperties['assumeMinServerVersion'] == '9.0' + + cleanup: + applicationContext.close() + } + + void "test individual datasource properties override global properties"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.default.data-source-properties.ApplicationName': 'SpecificApp', + 'datasources.default.data-source-properties.specificProperty': 'specificValue', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.globalProperty': 'globalValue'] + )) + applicationContext.start() + + when: + DatasourceConfiguration datasourceConfig = applicationContext.getBean(DatasourceConfiguration) + + then: "Individual properties override globals, but global properties are still added" + datasourceConfig != null + datasourceConfig.dbProperties['ApplicationName'] == 'SpecificApp' // Overridden + datasourceConfig.dbProperties['specificProperty'] == 'specificValue' // Individual property + datasourceConfig.dbProperties['assumeMinServerVersion'] == '9.0' // Added from global + datasourceConfig.dbProperties['globalProperty'] == 'globalValue' // Added from global + + cleanup: + applicationContext.close() + } +} diff --git a/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceConfigModifier.java b/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceConfigModifier.java new file mode 100644 index 000000000..38b584766 --- /dev/null +++ b/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceConfigModifier.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.ucp; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.context.exceptions.ConfigurationException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.sql.SQLException; +import java.util.Properties; + +/** + * A bean created event listener that applies global datasource properties to + * {@link DatasourceConfiguration} beans when they are created. This modifier + * ensures that global properties defined under the "global.datasources.data-source-properties" + * configuration prefix are automatically applied to all datasource configurations, + * while preserving individual datasource-specific settings that take precedence. + * + *

The modifier only adds global properties that are not already present in the + * individual datasource configuration, ensuring that specific configurations always + * override global defaults. Properties with null values are ignored.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@Singleton +public class GlobalDatasourceConfigModifier implements BeanCreatedEventListener { + + @Inject + GlobalDatasourceProperties globalDatasourceProperties; + + @Override + public DatasourceConfiguration onCreated(BeanCreatedEvent event) { + + DatasourceConfiguration configuration = event.getBean(); + Properties existingProperties = configuration.delegate.getConnectionProperties(); + if (existingProperties == null) { + existingProperties = new Properties(); + } + + final Properties finalExistingProperties = existingProperties; + globalDatasourceProperties.getDataSourceProperties() + .forEach((key, value) -> { + if (value != null && !finalExistingProperties.containsKey(key)) { + finalExistingProperties.setProperty(key, value.toString()); + } + } + ); + + try { + configuration.delegate.setConnectionProperties(finalExistingProperties); + } catch (SQLException e) { + throw new ConfigurationException("Unable to set global datasource properties: " + e.getMessage(), e); + } + + return configuration; + } +} diff --git a/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceProperties.java b/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceProperties.java new file mode 100644 index 000000000..7d07fba2d --- /dev/null +++ b/jdbc-ucp/src/main/java/io/micronaut/configuration/jdbc/ucp/GlobalDatasourceProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ +package io.micronaut.configuration.jdbc.ucp; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.naming.conventions.StringConvention; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for global datasource settings that can be applied + * to all {@link DatasourceConfiguration} instances. This class binds configuration + * properties under the "global.datasources" prefix and provides a way to define + * common datasource properties that should be applied across all datasources + * in the application. + * + *

Properties defined here serve as defaults that can be overridden by + * individual datasource configurations. The primary use case is to avoid + * repetition when multiple datasources share common settings such as + * connection pool parameters, SSL settings, or application-specific properties.

+ * + *

This bean is only created when the "global.datasources.data-source-properties" + * configuration property is present, ensuring it doesn't interfere with applications + * that don't use global datasource configuration.

+ * + * @author James Forward + */ +@Requires(property = "global.datasources.data-source-properties") +@ConfigurationProperties("global.datasources") +public class GlobalDatasourceProperties { + private Map dataSourceProperties = new HashMap<>(); + + public Map getDataSourceProperties() { + return dataSourceProperties; + } + + public void setDataSourceProperties(@MapFormat(transformation = MapFormat.MapTransformation.FLAT, keyFormat = StringConvention.RAW) Map dataSourceProperties) { + this.dataSourceProperties.putAll(dataSourceProperties); + } +} diff --git a/jdbc-ucp/src/test/groovy/io/micronaut/configuration/jdbc/ucp/GlobalDatasourcePropertiesSpec.groovy b/jdbc-ucp/src/test/groovy/io/micronaut/configuration/jdbc/ucp/GlobalDatasourcePropertiesSpec.groovy new file mode 100644 index 000000000..71ba87756 --- /dev/null +++ b/jdbc-ucp/src/test/groovy/io/micronaut/configuration/jdbc/ucp/GlobalDatasourcePropertiesSpec.groovy @@ -0,0 +1,131 @@ +package io.micronaut.configuration.jdbc.ucp +/* + * Copyright 2017-2020 original authors + * + * 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 + * + * https://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. + */ + + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.MapPropertySource +import spock.lang.Specification + +class GlobalDatasourcePropertiesSpec extends Specification { + + void "test no global datasource configuration exists when no global properties are present"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default'] + )) + applicationContext.start() + + when: + Optional properties = applicationContext.findBean(GlobalDatasourceProperties) + Optional datasourceConfig = applicationContext.findBean(DatasourceConfiguration) + + then: "No global beans are created when no global configuration is present" + datasourceConfig.isPresent() + datasourceConfig.get().delegate.connectionProperties.isEmpty() + datasourceConfig.get().url == 'jdbc:h2:mem:default' + properties.isEmpty() + + cleanup: + applicationContext.close() + } + + void "test global datasource properties configuration creates correct beans"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['global.datasources.data-source-properties.ApplicationName': 'MyApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.reWriteBatchInserts': true] + )) + applicationContext.start() + + when: + GlobalDatasourceProperties properties = applicationContext.getBean(GlobalDatasourceProperties) + + then: "GlobalDatasourceProperties bean is created with correct properties" + properties != null + properties.dataSourceProperties != null + properties.dataSourceProperties.size() == 3 + properties.dataSourceProperties['ApplicationName'] == 'MyApp' + properties.dataSourceProperties['assumeMinServerVersion'] == '9.0' + properties.dataSourceProperties['reWriteBatchInserts'] == true + + and: "GlobalDatasourceConfigModifier bean is also created" + applicationContext.containsBean(GlobalDatasourceConfigModifier) + + cleanup: + applicationContext.close() + } + + void "test global properties are applied to all datasource configurations"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.secondary.url': 'jdbc:h2:mem:secondary', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0'] + )) + applicationContext.start() + + when: + def datasourceConfigs = applicationContext.getBeansOfType(DatasourceConfiguration) + + then: "Global properties are applied to all DatasourceConfiguration beans" + datasourceConfigs.size() == 2 + datasourceConfigs[0].delegate.connectionProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[0].delegate.connectionProperties['assumeMinServerVersion'] == '9.0' + datasourceConfigs[1].delegate.connectionProperties['ApplicationName'] == 'GlobalApp' + datasourceConfigs[1].delegate.connectionProperties['assumeMinServerVersion'] == '9.0' + + cleanup: + applicationContext.close() + } + + void "test individual datasource properties override global properties"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(MapPropertySource.of( + 'test', + ['datasources.default.url': 'jdbc:h2:mem:default', + 'datasources.default.data-source-properties.ApplicationName': 'SpecificApp', + 'datasources.default.data-source-properties.specificProperty': 'specificValue', + 'global.datasources.data-source-properties.ApplicationName': 'GlobalApp', + 'global.datasources.data-source-properties.assumeMinServerVersion': '9.0', + 'global.datasources.data-source-properties.globalProperty': 'globalValue'] + )) + applicationContext.start() + + when: + DatasourceConfiguration datasourceConfig = applicationContext.getBean(DatasourceConfiguration) + + then: "Individual properties override globals, but global properties are still added" + datasourceConfig != null + datasourceConfig.delegate.connectionProperties['ApplicationName'] == 'SpecificApp' // Overridden + datasourceConfig.delegate.connectionProperties['specificProperty'] == 'specificValue' // Individual property + datasourceConfig.delegate.connectionProperties['assumeMinServerVersion'] == '9.0' // Added from global + datasourceConfig.delegate.connectionProperties['globalProperty'] == 'globalValue' // Added from global + + cleanup: + applicationContext.close() + } +}