From 4c224a8d16bad70121d2a4484e9e07f930b94846 Mon Sep 17 00:00:00 2001 From: Ramesh Malla Date: Mon, 8 Jan 2024 12:44:45 +0100 Subject: [PATCH 1/5] Support contextprovider as a method parameter --- ...eConfigurationMethodInvocationHandler.java | 18 +++++- .../baigan/e2e/configs/SomeConfiguration.java | 2 + .../e2e/filerepo/CustomContextProvider.java | 27 +++++++++ ...nfigurationContextProviderEnd2EndTest.java | 56 +++++++++++++++++++ src/test/resources/test-config.json | 52 +++++++++++++++++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/zalando/baigan/e2e/filerepo/CustomContextProvider.java create mode 100644 src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java create mode 100644 src/test/resources/test-config.json diff --git a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java index f27565a..dce2abc 100644 --- a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java +++ b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java @@ -14,10 +14,13 @@ import org.zalando.baigan.repository.ConfigurationRepository; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Suppliers.memoize; @@ -57,7 +60,13 @@ public void setBeanFactory(final BeanFactory beanFactory) throws BeansException @Override protected Object handleInvocation(Object proxy, Method method, Object[] args) { final String key = createKey(getClass(proxy), method); - final Object result = getConfig(key); + + final List contextProviders = Arrays.stream(args) + .filter(ContextProvider.class::isInstance) + .map(ContextProvider.class::cast) + .collect(Collectors.toList()); + + final Object result = getConfig(key,contextProviders); if (result == null) { LOG.warn("Configuration not found for key: {}", key); return null; @@ -76,7 +85,7 @@ private Class getClass(final Object proxy) { return interfaces[0]; } - private Object getConfig(final String key) { + private Object getConfig(final String key, final List contextProviders) { final Optional optional = configurationRepository.get().get(key); if (!optional.isPresent()) { @@ -95,6 +104,11 @@ private Object getConfig(final String key) { context.put(param, provider.getContextParam(param)); } + contextProviders.forEach(provider -> + provider.getProvidedContexts() + .forEach(param -> context.put(param, provider.getContextParam(param))) + ); + return conditionsProcessor.get().process(optional.get(), context); } diff --git a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java index 24a57de..25bcd1e 100644 --- a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java +++ b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java @@ -1,6 +1,7 @@ package org.zalando.baigan.e2e.configs; import org.zalando.baigan.annotation.BaiganConfig; +import org.zalando.baigan.e2e.filerepo.CustomContextProvider; import java.util.List; import java.util.Map; @@ -11,6 +12,7 @@ public interface SomeConfiguration { SomeConfigObject someConfig(); String someValue(); Boolean isThisTrue(); + Boolean toggleFlag(CustomContextProvider customContextProvider); Map> topLevelGenerics(); List configList(); } diff --git a/src/test/java/org/zalando/baigan/e2e/filerepo/CustomContextProvider.java b/src/test/java/org/zalando/baigan/e2e/filerepo/CustomContextProvider.java new file mode 100644 index 0000000..08831d2 --- /dev/null +++ b/src/test/java/org/zalando/baigan/e2e/filerepo/CustomContextProvider.java @@ -0,0 +1,27 @@ +package org.zalando.baigan.e2e.filerepo; + +import org.jetbrains.annotations.NotNull; +import org.zalando.baigan.context.ContextProvider; + +import java.util.Set; + +public class CustomContextProvider implements ContextProvider { + + private final Set PARAMS = Set.of("appdomain"); + + private final String appDomain; + + public CustomContextProvider(String appDomain) { + this.appDomain = appDomain; + } + + @Override + public String getContextParam(@NotNull final String name) { + return appDomain; + } + + @Override + public Set getProvidedContexts() { + return PARAMS; + } +} diff --git a/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java new file mode 100644 index 0000000..efea80b --- /dev/null +++ b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java @@ -0,0 +1,56 @@ +package org.zalando.baigan.e2e.filerepo; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.zalando.baigan.BaiganSpringContext; +import org.zalando.baigan.annotation.ConfigurationServiceScan; +import org.zalando.baigan.e2e.configs.SomeConfiguration; +import org.zalando.baigan.repository.FileSystemConfigurationRepository; +import org.zalando.baigan.repository.RepositoryFactory; + +import java.time.Duration; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {FileSystemConfigurationContextProviderEnd2EndTest.RepoConfig.class}) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class FileSystemConfigurationContextProviderEnd2EndTest { + + @Autowired + private SomeConfiguration someConfiguration; + + private static final Duration CONFIG_REFRESH_INTERVAL = Duration.ofMillis(100); + + @Test + public void testConfigurationsWithContext() { + assertThat(someConfiguration.toggleFlag(new CustomContextProvider("1")), equalTo(true)); + assertThat(someConfiguration.toggleFlag(new CustomContextProvider("2")), equalTo(false)); + assertThat(someConfiguration.toggleFlag(null), equalTo(false)); + } + + @ConfigurationServiceScan(basePackageClasses = SomeConfiguration.class) + @Testcontainers + @ComponentScan(basePackageClasses = {BaiganSpringContext.class}) + static class RepoConfig { + + @Bean + FileSystemConfigurationRepository configurationRepository(RepositoryFactory repositoryFactory) { + return repositoryFactory.fileSystemConfigurationRepository() + .fileName(FileSystemConfigurationContextProviderEnd2EndTest.class.getClassLoader().getResource("test-config.json").getPath()) + .refreshInterval(CONFIG_REFRESH_INTERVAL) + .objectMapper(new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false)) + .build(); + } + } +} diff --git a/src/test/resources/test-config.json b/src/test/resources/test-config.json new file mode 100644 index 0000000..7c2505d --- /dev/null +++ b/src/test/resources/test-config.json @@ -0,0 +1,52 @@ +[ + { + "alias": "some.non.existing.config", + "defaultValue": "an irrelevant value" + }, + { + "alias": "some.configuration.is.this.true", + "defaultValue": false, + "conditions": [ + { + "value": true, + "conditionType": { + "onValue": "1", + "type": "Equals" + }, + "paramName": "appdomain" + } + ] + }, + { + "alias": "some.configuration.some.value", + "defaultValue": "some value" + }, + { + "alias": "some.configuration.some.config", + "defaultValue": { + "config_key": "a value", + "extra_field": "objectMapper configured to not fail for unknown properties" + } + }, + { + "alias": "some.configuration.toggle.flag", + "defaultValue": false, + "conditions": [ + { + "value": true, + "conditionType": { + "onValue": "1", + "type": "Equals" + }, + "paramName": "appdomain" + } + ] + }, + { + "alias": "some.configuration.config.list", + "defaultValue": [ + "A", + "B" + ] + } +] \ No newline at end of file From 2b4c6a803c1e748776af77d88a7e1120142ef1db Mon Sep 17 00:00:00 2001 From: Ramesh Malla Date: Mon, 8 Jan 2024 18:56:34 +0100 Subject: [PATCH 2/5] Always take the first context provider --- ...areConfigurationMethodInvocationHandler.java | 17 ++++++++++++----- .../baigan/e2e/configs/SomeConfiguration.java | 2 +- ...ConfigurationContextProviderEnd2EndTest.java | 6 +++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java index dce2abc..e37b4ac 100644 --- a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java +++ b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java @@ -11,8 +11,10 @@ import org.zalando.baigan.context.ContextProviderRetriever; import org.zalando.baigan.model.Configuration; import org.zalando.baigan.context.ContextProvider; +import org.zalando.baigan.repository.ConfigurationParser; import org.zalando.baigan.repository.ConfigurationRepository; +import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -66,7 +68,7 @@ protected Object handleInvocation(Object proxy, Method method, Object[] args) { .map(ContextProvider.class::cast) .collect(Collectors.toList()); - final Object result = getConfig(key,contextProviders); + final Object result = getConfig(key, contextProviders); if (result == null) { LOG.warn("Configuration not found for key: {}", key); return null; @@ -104,10 +106,15 @@ private Object getConfig(final String key, final List contextPr context.put(param, provider.getContextParam(param)); } - contextProviders.forEach(provider -> - provider.getProvidedContexts() - .forEach(param -> context.put(param, provider.getContextParam(param))) - ); + if (!CollectionUtils.isEmpty(contextProviders)) { + if (contextProviders.size() > 1) { + LOG.warn("The key [{}] has more than one context provider, and therefore only the first context will be used", key); + } + final ContextProvider contextProvider = contextProviders.get(0); + contextProvider + .getProvidedContexts() + .forEach(param -> context.put(param, contextProvider.getContextParam(param))); + } return conditionsProcessor.get().process(optional.get(), context); diff --git a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java index 25bcd1e..b30dba3 100644 --- a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java +++ b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java @@ -12,7 +12,7 @@ public interface SomeConfiguration { SomeConfigObject someConfig(); String someValue(); Boolean isThisTrue(); - Boolean toggleFlag(CustomContextProvider customContextProvider); + Boolean toggleFlag(CustomContextProvider customContextProvider,CustomContextProvider secondProvider); Map> topLevelGenerics(); List configList(); } diff --git a/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java index efea80b..3acfbbd 100644 --- a/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java +++ b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java @@ -34,9 +34,9 @@ public class FileSystemConfigurationContextProviderEnd2EndTest { @Test public void testConfigurationsWithContext() { - assertThat(someConfiguration.toggleFlag(new CustomContextProvider("1")), equalTo(true)); - assertThat(someConfiguration.toggleFlag(new CustomContextProvider("2")), equalTo(false)); - assertThat(someConfiguration.toggleFlag(null), equalTo(false)); + assertThat(someConfiguration.toggleFlag(new CustomContextProvider("1"),new CustomContextProvider("3")), equalTo(true)); + assertThat(someConfiguration.toggleFlag(new CustomContextProvider("2"),new CustomContextProvider("1")), equalTo(false)); + assertThat(someConfiguration.toggleFlag(null,null), equalTo(false)); } @ConfigurationServiceScan(basePackageClasses = SomeConfiguration.class) From a00b7d2d4122bb1e5dccdd7646dcfbc95633d5a5 Mon Sep 17 00:00:00 2001 From: Ramesh Malla Date: Mon, 22 Jan 2024 14:56:30 +0100 Subject: [PATCH 3/5] Updated readme to refer to wiki --- README.md | 195 +----------------- .../baigan/e2e/configs/SomeConfiguration.java | 3 +- 2 files changed, 4 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index f3d1d13..56d0126 100644 --- a/README.md +++ b/README.md @@ -3,200 +3,9 @@ ![Build Status](https://github.com/zalando-stups/baigan-config/workflows/build/badge.svg) [![Maven Central](https://img.shields.io/maven-central/v/org.zalando/baigan-config.svg)](https://maven-badges.herokuapp.com/maven-central/org.zalando/baigan-config) -Baigan configuration is an easy-to-use configuration framework for [Spring](https://spring.io/) based applications. +Baigan configuration is an easy-to-use configuration framework for [Spring](https://spring.io/) based applications. -What makes Baigan a rockstar configuration framework ? - -* *Simple*: Using Baigan configurations is as simple as annotating a Java interface. -* *Extensible*: Extend configurations, create rules, define types that suit you. -* *Flexible*: Baigan is a client library that can read configurations from multiple repositories: - * Filesystem - * AWS S3 - -## Prerequisites -- Java 17+ -- Spring Framework 6 (backwards compatible to 5) -- AWS SDK - -## Getting started - -### To build the project run: - -```bash -./mvnw clean install -Pintegration-test -``` - -### Integrating Baigan config -Baigan config is a spring project. The larger part of integration involves configuring beans to facilitate the spring beans. - -#### Configuring components and Configuration interface scanning. - -```Java - -import org.zalando.baigan.BaiganSpringContext; - -@ComponentScan(basePackageClasses = { BaiganSpringContext.class }) -@ConfigurationServiceScan(basePackages = { "com.foo.configurations" }) -public class Application { -} -``` - -The _BaiganSpringContext_ class includes the Baigan-Config beans required to be loaded into the spring application context. -And the _@ConfigurationServiceScan_ annotation hints the Baigan registrar to look into the packages where the _@BaiganConfig_ annotated interfaces could be found. - -#### Annotate your configuration interfaces with _@BaiganConfig_ - -```Java -@BaiganConfig -public interface ExpressFeature { - - Boolean enabled(); - - String serviceUrl(); - - SomeStructuredConfigClass complexConfiguration(); - - List configList(); - - Map> nestedGenericConfiguration(); -} -``` - -The individual methods may have arbitrary classes as return types, in particular complex structured types are supported, including Generics. - -**Note**: Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value. - -> [!CAUTION] -> Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value. -> If you use Baigan with Kotlin, it means you need to use nullable primitive types, e.g. `Int?` instead of `Int`. - -The above example code enables the application to inject _ExpressFeature_ spring bean into any other Spring bean: - -```Java -@Component -public class ExpressServiceImpl implements ExpressService { - - @Inject - private ExpressFeature expressFeature; - - @Override - public void sendShipment(final Shipment shipment) { - if (expressFeature.enabled()) { - final String serviceUrl = expressFeature.serviceUrl(); - // Use the configuration - } - } -} -``` -> [!CAUTION] -> Due to the way bean access is managed, concurrent use of Baigan's proxies from multiple threads during the early stages of Spring context initialization can result in concurrency issues, including the potential for deadlock. -> To mitigate this risk, it is advisable to refrain from accessing Baigan's proxies until the Spring context has been initialized. - - -#### Provide a configuration repository - -Finally, a `ConfigurationRepository` Spring Bean has to be provided that can provide the configuration values. -This is done using the Spring Bean of type `RepositoryFactory`, which allows creating builders for all repository -types. The following example shows how to configure a filesystem based repository. - -```Java -@Configuration -public class ApplicationConfiguration { - - @Bean - public ConfigurationRepository configurationRepository(RepositoryFactory factory) { - return factory.fileSystemConfigurationRepository() - .fileName("configs.json") - .build(); - } -} -``` - -Check the documentation of the builders for details on how to configure the repositories. In particular, all -repositories can be configured with a Jackson `ObjectMapper` used to deserialize the configuration. - -### Creating configurations -Baigan configurations follow a specific schema and can be stored on any of the supported repositories. - -#### Configuration schema -Configurations are stored in its simplest form as key values. -A configuration is a pair of a dot(.) separated key and a value objects in JSON format. - -A configuration object should conform to the following JSON Schema: - -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Configuration", - "description": "A baigan configuration object value.", - "type": "object", - "properties": { - "alias": { - "description": "The identifier for the configuration, same as its key.", - "type": "string" - }, - "description": { - "description": "Summary of the configuration.", - "type": "string" - }, - "defaultValue": { - "description": "Default configuration if none of the condition is satisfied.", - "type": {} - }, - "conditions": { - "description": "List of conditions to check", - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "description": "Configuration value if this condition evaluates to true.", - "type": {} - }, - "conditionType": { - "description": "Type of condition to evaluate. This can be custom defined, with custom defined properties.", - "type": "object" - } - } - } - } - }, - "required": ["defaultValue"] -} -``` - -#### Example configurations - -This sample JSON defines a configuration for the key `express.feature.enabled` with the value _true_ when the _country_code_ is 3, and a default value of _false_. - -```json -[ - { - "alias": "express.feature.enabled", - "description": "Feature toggle", - "defaultValue": false, - "conditions": [ - { - "value": true, - "conditionType": { - "onValue": "3", - "type": "Equals" - }, - "paramName": "country_code" - } - ] - } -] -``` - -#### Pushing configuration to repositories -This step depends on the chosen repository. - -##### Filesystem -Save a file named express-feature.json with the content above anywhere on the filesystem and bundle it as part of your application. To use it just specify the classpath in the constructor. - -##### AWS S3 -Save a file named express-feature.json with the content above and upload it to any S3 bucket. To use it just provide the bucket name and the object key. +Please refer to the [wiki](https://github.com/zalando-stups/baigan-config/wiki) to know more about usage, information, HOWTO, etc. ## 0.18.0 + 0.19.0 + 0.19.1 releases With certain JDK/JRE versions used, annotated configuration interfaces were not registered as beans. Be aware, that this issue does not occur when application code is being executed by a test runner or alike, only in production setups. Therefore, we recommend using a higher version to avoid this. diff --git a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java index b30dba3..b08547a 100644 --- a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java +++ b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java @@ -1,6 +1,7 @@ package org.zalando.baigan.e2e.configs; import org.zalando.baigan.annotation.BaiganConfig; +import org.zalando.baigan.context.ContextProvider; import org.zalando.baigan.e2e.filerepo.CustomContextProvider; import java.util.List; @@ -12,7 +13,7 @@ public interface SomeConfiguration { SomeConfigObject someConfig(); String someValue(); Boolean isThisTrue(); - Boolean toggleFlag(CustomContextProvider customContextProvider,CustomContextProvider secondProvider); + Boolean toggleFlag(ContextProvider customContextProvider, CustomContextProvider secondProvider); Map> topLevelGenerics(); List configList(); } From a3f571bdcd22b7d5e270025620bfcade912672ca Mon Sep 17 00:00:00 2001 From: Ramesh Malla Date: Mon, 22 Jan 2024 15:04:32 +0100 Subject: [PATCH 4/5] Revert "Updated readme to refer to wiki" This reverts commit a00b7d2d4122bb1e5dccdd7646dcfbc95633d5a5. --- README.md | 195 +++++++++++++++++- .../baigan/e2e/configs/SomeConfiguration.java | 3 +- 2 files changed, 194 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56d0126..f3d1d13 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,200 @@ ![Build Status](https://github.com/zalando-stups/baigan-config/workflows/build/badge.svg) [![Maven Central](https://img.shields.io/maven-central/v/org.zalando/baigan-config.svg)](https://maven-badges.herokuapp.com/maven-central/org.zalando/baigan-config) -Baigan configuration is an easy-to-use configuration framework for [Spring](https://spring.io/) based applications. +Baigan configuration is an easy-to-use configuration framework for [Spring](https://spring.io/) based applications. -Please refer to the [wiki](https://github.com/zalando-stups/baigan-config/wiki) to know more about usage, information, HOWTO, etc. +What makes Baigan a rockstar configuration framework ? + +* *Simple*: Using Baigan configurations is as simple as annotating a Java interface. +* *Extensible*: Extend configurations, create rules, define types that suit you. +* *Flexible*: Baigan is a client library that can read configurations from multiple repositories: + * Filesystem + * AWS S3 + +## Prerequisites +- Java 17+ +- Spring Framework 6 (backwards compatible to 5) +- AWS SDK + +## Getting started + +### To build the project run: + +```bash +./mvnw clean install -Pintegration-test +``` + +### Integrating Baigan config +Baigan config is a spring project. The larger part of integration involves configuring beans to facilitate the spring beans. + +#### Configuring components and Configuration interface scanning. + +```Java + +import org.zalando.baigan.BaiganSpringContext; + +@ComponentScan(basePackageClasses = { BaiganSpringContext.class }) +@ConfigurationServiceScan(basePackages = { "com.foo.configurations" }) +public class Application { +} +``` + +The _BaiganSpringContext_ class includes the Baigan-Config beans required to be loaded into the spring application context. +And the _@ConfigurationServiceScan_ annotation hints the Baigan registrar to look into the packages where the _@BaiganConfig_ annotated interfaces could be found. + +#### Annotate your configuration interfaces with _@BaiganConfig_ + +```Java +@BaiganConfig +public interface ExpressFeature { + + Boolean enabled(); + + String serviceUrl(); + + SomeStructuredConfigClass complexConfiguration(); + + List configList(); + + Map> nestedGenericConfiguration(); +} +``` + +The individual methods may have arbitrary classes as return types, in particular complex structured types are supported, including Generics. + +**Note**: Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value. + +> [!CAUTION] +> Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value. +> If you use Baigan with Kotlin, it means you need to use nullable primitive types, e.g. `Int?` instead of `Int`. + +The above example code enables the application to inject _ExpressFeature_ spring bean into any other Spring bean: + +```Java +@Component +public class ExpressServiceImpl implements ExpressService { + + @Inject + private ExpressFeature expressFeature; + + @Override + public void sendShipment(final Shipment shipment) { + if (expressFeature.enabled()) { + final String serviceUrl = expressFeature.serviceUrl(); + // Use the configuration + } + } +} +``` +> [!CAUTION] +> Due to the way bean access is managed, concurrent use of Baigan's proxies from multiple threads during the early stages of Spring context initialization can result in concurrency issues, including the potential for deadlock. +> To mitigate this risk, it is advisable to refrain from accessing Baigan's proxies until the Spring context has been initialized. + + +#### Provide a configuration repository + +Finally, a `ConfigurationRepository` Spring Bean has to be provided that can provide the configuration values. +This is done using the Spring Bean of type `RepositoryFactory`, which allows creating builders for all repository +types. The following example shows how to configure a filesystem based repository. + +```Java +@Configuration +public class ApplicationConfiguration { + + @Bean + public ConfigurationRepository configurationRepository(RepositoryFactory factory) { + return factory.fileSystemConfigurationRepository() + .fileName("configs.json") + .build(); + } +} +``` + +Check the documentation of the builders for details on how to configure the repositories. In particular, all +repositories can be configured with a Jackson `ObjectMapper` used to deserialize the configuration. + +### Creating configurations +Baigan configurations follow a specific schema and can be stored on any of the supported repositories. + +#### Configuration schema +Configurations are stored in its simplest form as key values. +A configuration is a pair of a dot(.) separated key and a value objects in JSON format. + +A configuration object should conform to the following JSON Schema: + +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Configuration", + "description": "A baigan configuration object value.", + "type": "object", + "properties": { + "alias": { + "description": "The identifier for the configuration, same as its key.", + "type": "string" + }, + "description": { + "description": "Summary of the configuration.", + "type": "string" + }, + "defaultValue": { + "description": "Default configuration if none of the condition is satisfied.", + "type": {} + }, + "conditions": { + "description": "List of conditions to check", + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "description": "Configuration value if this condition evaluates to true.", + "type": {} + }, + "conditionType": { + "description": "Type of condition to evaluate. This can be custom defined, with custom defined properties.", + "type": "object" + } + } + } + } + }, + "required": ["defaultValue"] +} +``` + +#### Example configurations + +This sample JSON defines a configuration for the key `express.feature.enabled` with the value _true_ when the _country_code_ is 3, and a default value of _false_. + +```json +[ + { + "alias": "express.feature.enabled", + "description": "Feature toggle", + "defaultValue": false, + "conditions": [ + { + "value": true, + "conditionType": { + "onValue": "3", + "type": "Equals" + }, + "paramName": "country_code" + } + ] + } +] +``` + +#### Pushing configuration to repositories +This step depends on the chosen repository. + +##### Filesystem +Save a file named express-feature.json with the content above anywhere on the filesystem and bundle it as part of your application. To use it just specify the classpath in the constructor. + +##### AWS S3 +Save a file named express-feature.json with the content above and upload it to any S3 bucket. To use it just provide the bucket name and the object key. ## 0.18.0 + 0.19.0 + 0.19.1 releases With certain JDK/JRE versions used, annotated configuration interfaces were not registered as beans. Be aware, that this issue does not occur when application code is being executed by a test runner or alike, only in production setups. Therefore, we recommend using a higher version to avoid this. diff --git a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java index b08547a..b30dba3 100644 --- a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java +++ b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java @@ -1,7 +1,6 @@ package org.zalando.baigan.e2e.configs; import org.zalando.baigan.annotation.BaiganConfig; -import org.zalando.baigan.context.ContextProvider; import org.zalando.baigan.e2e.filerepo.CustomContextProvider; import java.util.List; @@ -13,7 +12,7 @@ public interface SomeConfiguration { SomeConfigObject someConfig(); String someValue(); Boolean isThisTrue(); - Boolean toggleFlag(ContextProvider customContextProvider, CustomContextProvider secondProvider); + Boolean toggleFlag(CustomContextProvider customContextProvider,CustomContextProvider secondProvider); Map> topLevelGenerics(); List configList(); } From 1adcf04d5abe9db795aeeeef1c80a97f5f488296 Mon Sep 17 00:00:00 2001 From: Ramesh Malla Date: Wed, 28 Feb 2024 17:54:59 +0100 Subject: [PATCH 5/5] handle duplicate contexts --- ...eConfigurationMethodInvocationHandler.java | 17 +++++++------ .../baigan/e2e/configs/SomeConfiguration.java | 1 - .../e2e/configs/TestContextConfiguration.java | 15 ++++++++++++ ...nfigurationContextProviderEnd2EndTest.java | 19 +++++++++------ src/test/resources/test-config.json | 24 +++---------------- 5 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 src/test/java/org/zalando/baigan/e2e/configs/TestContextConfiguration.java diff --git a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java index e37b4ac..8063741 100644 --- a/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java +++ b/src/main/java/org/zalando/baigan/proxy/handler/ContextAwareConfigurationMethodInvocationHandler.java @@ -107,13 +107,16 @@ private Object getConfig(final String key, final List contextPr } if (!CollectionUtils.isEmpty(contextProviders)) { - if (contextProviders.size() > 1) { - LOG.warn("The key [{}] has more than one context provider, and therefore only the first context will be used", key); - } - final ContextProvider contextProvider = contextProviders.get(0); - contextProvider - .getProvidedContexts() - .forEach(param -> context.put(param, contextProvider.getContextParam(param))); + contextProviders.forEach(contextProvider -> { + contextProvider + .getProvidedContexts() + .forEach(contextParam -> { + if(context.containsKey(contextParam)){ + throw new RuntimeException("Cannot have more than one context provider for the same context key "+contextParam); + } + context.put(contextParam, contextProvider.getContextParam(contextParam)); + }); + }); } return conditionsProcessor.get().process(optional.get(), context); diff --git a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java index b30dba3..f50f6d1 100644 --- a/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java +++ b/src/test/java/org/zalando/baigan/e2e/configs/SomeConfiguration.java @@ -12,7 +12,6 @@ public interface SomeConfiguration { SomeConfigObject someConfig(); String someValue(); Boolean isThisTrue(); - Boolean toggleFlag(CustomContextProvider customContextProvider,CustomContextProvider secondProvider); Map> topLevelGenerics(); List configList(); } diff --git a/src/test/java/org/zalando/baigan/e2e/configs/TestContextConfiguration.java b/src/test/java/org/zalando/baigan/e2e/configs/TestContextConfiguration.java new file mode 100644 index 0000000..e291f8a --- /dev/null +++ b/src/test/java/org/zalando/baigan/e2e/configs/TestContextConfiguration.java @@ -0,0 +1,15 @@ +package org.zalando.baigan.e2e.configs; + +import org.zalando.baigan.annotation.BaiganConfig; +import org.zalando.baigan.e2e.filerepo.CustomContextProvider; + +@BaiganConfig +public interface TestContextConfiguration { + + String someValue(); + + Boolean isThisTrue(CustomContextProvider customContextProvider); + + Boolean toggleFlag(CustomContextProvider customContextProvider, CustomContextProvider secondProvider); + +} \ No newline at end of file diff --git a/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java index 3acfbbd..ad68a38 100644 --- a/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java +++ b/src/test/java/org/zalando/baigan/e2e/filerepo/FileSystemConfigurationContextProviderEnd2EndTest.java @@ -12,7 +12,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.zalando.baigan.BaiganSpringContext; import org.zalando.baigan.annotation.ConfigurationServiceScan; -import org.zalando.baigan.e2e.configs.SomeConfiguration; +import org.zalando.baigan.e2e.configs.TestContextConfiguration; import org.zalando.baigan.repository.FileSystemConfigurationRepository; import org.zalando.baigan.repository.RepositoryFactory; @@ -21,6 +21,7 @@ import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {FileSystemConfigurationContextProviderEnd2EndTest.RepoConfig.class}) @@ -28,18 +29,22 @@ public class FileSystemConfigurationContextProviderEnd2EndTest { @Autowired - private SomeConfiguration someConfiguration; + private TestContextConfiguration testContextConfiguration; private static final Duration CONFIG_REFRESH_INTERVAL = Duration.ofMillis(100); @Test - public void testConfigurationsWithContext() { - assertThat(someConfiguration.toggleFlag(new CustomContextProvider("1"),new CustomContextProvider("3")), equalTo(true)); - assertThat(someConfiguration.toggleFlag(new CustomContextProvider("2"),new CustomContextProvider("1")), equalTo(false)); - assertThat(someConfiguration.toggleFlag(null,null), equalTo(false)); + public void testConfigurationsWithMultipleContextsHavingTheSameKeyShouldFail() { + assertThrows(RuntimeException.class, () -> testContextConfiguration.toggleFlag(new CustomContextProvider("1"), new CustomContextProvider("3"))); } - @ConfigurationServiceScan(basePackageClasses = SomeConfiguration.class) + @Test + public void testConfigurationsWithMultipleContexts() { + assertThat(testContextConfiguration.isThisTrue(new CustomContextProvider("1")), equalTo(true)); + assertThat(testContextConfiguration.someValue(), equalTo("some value")); + } + + @ConfigurationServiceScan(basePackageClasses = TestContextConfiguration.class) @Testcontainers @ComponentScan(basePackageClasses = {BaiganSpringContext.class}) static class RepoConfig { diff --git a/src/test/resources/test-config.json b/src/test/resources/test-config.json index 7c2505d..661c376 100644 --- a/src/test/resources/test-config.json +++ b/src/test/resources/test-config.json @@ -1,10 +1,6 @@ [ { - "alias": "some.non.existing.config", - "defaultValue": "an irrelevant value" - }, - { - "alias": "some.configuration.is.this.true", + "alias": "test.context.configuration.is.this.true", "defaultValue": false, "conditions": [ { @@ -18,18 +14,11 @@ ] }, { - "alias": "some.configuration.some.value", + "alias": "test.context.configuration.some.value", "defaultValue": "some value" }, { - "alias": "some.configuration.some.config", - "defaultValue": { - "config_key": "a value", - "extra_field": "objectMapper configured to not fail for unknown properties" - } - }, - { - "alias": "some.configuration.toggle.flag", + "alias": "test.context.configuration.toggle.flag", "defaultValue": false, "conditions": [ { @@ -41,12 +30,5 @@ "paramName": "appdomain" } ] - }, - { - "alias": "some.configuration.config.list", - "defaultValue": [ - "A", - "B" - ] } ] \ No newline at end of file