diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cd27d..b960e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.0.1] - 2024-03-21 +## [0.0.2] - 2024-03-27 -### Added - -* A bean to ask the current environment from. +Initial version. \ No newline at end of file diff --git a/README.md b/README.md index b15d970..f0ee4a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Wise Environment. ![Apache 2](https://img.shields.io/hexpm/l/plug.svg) -![Java 11](https://img.shields.io/badge/Java-11-blue.svg) +![Java 17](https://img.shields.io/badge/Java-17-blue.svg) ![Maven Central](https://badgen.net/maven/v/maven-central/com.transferwise.common/wise-environment) [![Owners](https://img.shields.io/badge/team-AppEng-blueviolet.svg?logo=wise)](https://transferwise.atlassian.net/wiki/spaces/EKB/pages/2520812116/Application+Engineering+Team) [![Slack](https://img.shields.io/badge/slack-sre--guild-blue.svg?logo=slack)](https://app.slack.com/client/T026FB76G/CLR1U8SNS) > Use the `@application-engineering-on-call` handle on Slack for help. @@ -9,15 +9,41 @@ Provides information to other libraries in which environment they are running in. -Allows to set default properties via environment variables and system properties. +Allows to set default properties for specific environments. -Those have to prefixed respectively with -* `WISE_DEFAULTS_` -* `wise.defaults.` +Typical use case is for various Wise libraries to set environment specific default properties in their `EnvironmentPostProcessor` implementations. -E.g. -* `WISE_DEFAULTS_WISE_ENVIRONMENT_TEST_VALUE1` -* `wise.defaults.wise.environment.test.value4` +```java +WiseEnvironment.setDefaultProperty("my-library", WiseEnvironment.PRODUCTION, "tw-reliable-jdbc.sslMode", SslMode.VERIFY_FULL); +WiseEnvironment.setDefaultProperty("my-library", WiseEnvironment.STAGING, "tw-reliable-jdbc.sslMode", SslMode.PREFERRED); +WiseEnvironment.setDefaultProperty("my-library", WiseEnvironment.CUSTOM_ENVIRONMENT, "tw-reliable-jdbc.sslMode", SslMode.VERIFY_CA); +``` + +See `WiseEnvironment` class for other optional use cases. E.g. asking which environments are currently active. + +The environments themselves can be hierarchical. In that sense, that if you set a default property to `STAGING`, it would also apply to +`CUSTOM_ENVIRONMENT`, unless a different value is specifically set for `CUSTOM_ENVIRONMENT`. + +Also, a convenience DSL is available for setting properties. E.g. + +```java +WiseEnvironment.setDefaultProperties(dsl -> dsl + .source("tw-reliable-jdbc") + + .environment(WiseEnvironment.WISE) + .set(TW_OBS_BASE_EXTREMUM_CONFIG_PATH, gaugeNames) + .set("spring.flyway.validate-migration-naming", "true") + + .keyPrefix("tw-reliable-jdbc.") + .environment(WiseEnvironment.PRODUCTION) + .set("sslMode", SslMode.VERIFY_FULL) + .set("requiredSslModeLevel", SslMode.VERIFY_CA) + .set("requireMinPoolSizePct", 100d) + + .environment(WiseEnvironment.STAGING) + .set("sslMode", SslMode.VERIFY_FULL) +); +``` ## License Copyright 2024 TransferWise Ltd. diff --git a/build.common.gradle b/build.common.gradle index 616764c..22ae3c8 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -12,7 +12,6 @@ apply plugin: "java-library" apply plugin: "checkstyle" apply plugin: "idea" apply plugin: "com.github.spotbugs" -apply plugin: "groovy" apply from: "$rootProject.rootDir/build.libraries.gradle" @@ -91,12 +90,6 @@ compileTestJava { test { useJUnitPlatform() - environment("WISE_DEFAULTS_WISE_ENVIRONMENT_TEST_VALUE1", "foo") - environment("WISE_DEFAULTS_WISE_ENVIRONMENT_TEST_VALUE2", "foo") - environment("WISE_ENVIRONMENT_TEST_VALUE3", "foo") - - systemProperty("wise.defaults.wise.environment.test.value4", "foo") - testLogging { events TestLogEvent.STARTED, TestLogEvent.FAILED, TestLogEvent.SKIPPED, TestLogEvent.PASSED, TestLogEvent.STANDARD_ERROR diff --git a/build.libraries.gradle b/build.libraries.gradle index 5df390b..7bf4d83 100644 --- a/build.libraries.gradle +++ b/build.libraries.gradle @@ -7,8 +7,9 @@ ext { springBootDependencies: "org.springframework.boot:spring-boot-dependencies:${springBootVersion}", // versions managed by spring-boot-dependencies platform + commonsLang3 : 'org.apache.commons:commons-lang3', lombok : 'org.projectlombok:lombok', springBootStarter : "org.springframework.boot:spring-boot-starter", - springBootStarterTest : "org.springframework.boot:spring-boot-starter-test" + springBootStarterTest : "org.springframework.boot:spring-boot-starter-test", ] } diff --git a/gradle.properties b/gradle.properties index 9759e31..c623543 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.0.1 \ No newline at end of file +version=0.0.2 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22c..b82aa23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 6689b85..7101f8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/wise-environment-starter/build.gradle b/wise-environment-starter/build.gradle index 8c64afe..64f47dc 100644 --- a/wise-environment-starter/build.gradle +++ b/wise-environment-starter/build.gradle @@ -1,10 +1,11 @@ -ext.projectArtifactName = "wise-environment" +ext.projectArtifactName = "wise-environment-starter" apply from: "$rootProject.rootDir/build.common.gradle" apply from: "$rootProject.rootDir/build.library.gradle" dependencies { - compileOnly libraries.springBootStarter + implementation libraries.commonsLang3 + implementation libraries.springBootStarter testImplementation libraries.springBootStarterTest } diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/CachingWiseEnvironmentProvider.java b/wise-environment-starter/src/main/java/com/wise/common/environment/CachingWiseEnvironmentProvider.java new file mode 100644 index 0000000..95bdc1f --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/CachingWiseEnvironmentProvider.java @@ -0,0 +1,39 @@ +package com.wise.common.environment; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CachingWiseEnvironmentProvider implements WiseEnvironmentProvider { + + private final List activeEnvironments; + + private Map isActiveCache = new ConcurrentHashMap<>(); + + public CachingWiseEnvironmentProvider(List activeEnvironments) { + this.activeEnvironments = activeEnvironments; + } + + @Override + public List getActiveEnvironments() { + return activeEnvironments; + } + + @Override + public boolean isEnvironmentActive(WiseEnvironment environment) { + return isActiveCache.computeIfAbsent(environment, k -> { + var activeEnvironments = getActiveEnvironments(); + + for (var activeEnvironment : activeEnvironments) { + while (activeEnvironment != null) { + if (activeEnvironment == environment) { + return true; + } + activeEnvironment = activeEnvironment.parent(); + } + } + + return false; + }); + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultPropertiesSetterDsl.java b/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultPropertiesSetterDsl.java new file mode 100644 index 0000000..d59ac25 --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultPropertiesSetterDsl.java @@ -0,0 +1,64 @@ +package com.wise.common.environment; + +public class DefaultPropertiesSetterDsl implements PropertiesSetterDsl { + + private String source; + private WiseEnvironment wiseEnvironment; + + private String keyPrefix; + + private Dsl0 dsl0 = new Dsl0Impl(); + private Dsl1 dsl1 = new Dsl1Impl(); + private Dsl2 dsl2 = new Dsl2Impl(); + + @Override + public Dsl0 source(String source) { + this.source = source; + return dsl0; + } + + class Dsl0Impl implements Dsl0 { + + @Override + public Dsl2 environment(WiseEnvironment wiseEnvironment) { + DefaultPropertiesSetterDsl.this.wiseEnvironment = wiseEnvironment; + return dsl2; + } + + @Override + public Dsl1 keyPrefix(String keyPrefix) { + DefaultPropertiesSetterDsl.this.keyPrefix = keyPrefix; + return dsl1; + } + } + + class Dsl1Impl implements Dsl1 { + + @Override + public Dsl2 environment(WiseEnvironment wiseEnvironment) { + DefaultPropertiesSetterDsl.this.wiseEnvironment = wiseEnvironment; + return dsl2; + } + } + + class Dsl2Impl implements Dsl2 { + + @Override + public Dsl2 environment(WiseEnvironment wiseEnvironment) { + DefaultPropertiesSetterDsl.this.wiseEnvironment = wiseEnvironment; + return this; + } + + @Override + public Dsl2 keyPrefix(String keyPrefix) { + DefaultPropertiesSetterDsl.this.keyPrefix = keyPrefix; + return this; + } + + @Override + public Dsl2 set(String name, Object value) { + WiseEnvironment.setDefaultProperty(source, wiseEnvironment, keyPrefix == null ? name : keyPrefix + name, value); + return this; + } + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironment.java b/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironment.java deleted file mode 100644 index 297695e..0000000 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironment.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.wise.common.environment; - -import com.wise.common.environment.WiseEnvironmentProperties.SubType; -import com.wise.common.environment.WiseEnvironmentProperties.Type; - -public class DefaultWiseEnvironment implements WiseEnvironment { - - private final Type type; - private final SubType subType; - - public DefaultWiseEnvironment(WiseEnvironmentProperties wiseEnvironmentProperties) { - var type = wiseEnvironmentProperties.getType(); - if (type == null) { - if (isExecutedByIntegrationTest()) { - type = Type.INTEGRATION_TEST; - } - } - if (type == null) { - throw new IllegalStateException("Wise environment type is not specified. You can do that through 'wise.environment.type' property."); - } - - this.type = type; - this.subType = wiseEnvironmentProperties.getSubType(); - } - - @Override - public boolean isProduction() { - return Type.PRODUCTION == type; - } - - @Override - public boolean isStaging() { - return Type.STAGING == type; - } - - @Override - public boolean isIntegrationTest() { - return Type.INTEGRATION_TEST == type; - } - - @Override - public boolean isDevelopment() { - return Type.DEVELOPMENT == type; - } - - @Override - public boolean isSandbox() { - return SubType.SANDBOX == subType; - } - - @Override - public boolean isCustomEnvironment() { - return SubType.CUSTOM_ENVS == subType; - } - - protected boolean isExecutedByIntegrationTest() { - for (var stackTraceElement : Thread.currentThread().getStackTrace()) { - if ("org.springframework.boot.test.context.SpringBootContextLoader".equals(stackTraceElement.getClassName())) { - return true; - } - } - return false; - } -} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironmentDetector.java b/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironmentDetector.java new file mode 100644 index 0000000..d2692d7 --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/DefaultWiseEnvironmentDetector.java @@ -0,0 +1,54 @@ +package com.wise.common.environment; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; + +public class DefaultWiseEnvironmentDetector implements WiseEnvironmentDetector { + + private final String value; + + public DefaultWiseEnvironmentDetector(String value) { + this.value = value; + } + + @Override + public List detect() { + final var result = new ArrayList(); + final var envs = StringUtils.split(value, ","); + if (envs != null) { + for (var env : envs) { + final var trimmedEnv = StringUtils.trim(env); + result.add(WiseEnvironment.getByName(trimmedEnv)); + } + } + + if (result.isEmpty()) { + if (isExecutedByIntegrationTest()) { + result.add(WiseEnvironment.INTEGRATION_TEST); + } else if (isExecutedByTest()) { + result.add(WiseEnvironment.UNIT_TEST); + } + } + + return result; + } + + protected boolean isExecutedByIntegrationTest() { + for (var stackTraceElement : Thread.currentThread().getStackTrace()) { + if ("org.springframework.boot.test.context.SpringBootContextLoader".equals(stackTraceElement.getClassName())) { + return true; + } + } + return false; + } + + protected boolean isExecutedByTest() { + for (var stackTraceElement : Thread.currentThread().getStackTrace()) { + if ("org.junit.platform.launcher.core.DefaultLauncher".equals(stackTraceElement.getClassName())) { + return true; + } + } + return false; + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/PropertiesSetterDsl.java b/wise-environment-starter/src/main/java/com/wise/common/environment/PropertiesSetterDsl.java new file mode 100644 index 0000000..dbc33ad --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/PropertiesSetterDsl.java @@ -0,0 +1,28 @@ +package com.wise.common.environment; + +public interface PropertiesSetterDsl { + + Dsl0 source(String source); + + interface Dsl0 { + + Dsl2 environment(WiseEnvironment environment); + + Dsl1 keyPrefix(String prefix); + } + + interface Dsl1 { + + Dsl2 environment(WiseEnvironment wiseEnvironment); + + } + + interface Dsl2 { + + Dsl2 environment(WiseEnvironment environment); + + Dsl2 keyPrefix(String keyPrefix); + + Dsl2 set(String property, Object value); + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/PropertyContainer.java b/wise-environment-starter/src/main/java/com/wise/common/environment/PropertyContainer.java new file mode 100644 index 0000000..ac88f80 --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/PropertyContainer.java @@ -0,0 +1,40 @@ +package com.wise.common.environment; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.origin.Origin; +import org.springframework.util.Assert; + +@Data +@Accessors(chain = true) +public class PropertyContainer { + + private Object value; + + private WiseEnvironmentOrigin origin; + + public PropertyContainer(String source, WiseEnvironment environment, Object value) { + Assert.notNull(value, "Value must not be null"); + this.origin = new WiseEnvironmentOrigin(source, environment); + this.value = value; + } + + @Data + public static class WiseEnvironmentOrigin implements Origin { + + private String source; + + private WiseEnvironment environment; + + public WiseEnvironmentOrigin(String source, WiseEnvironment environment) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(environment, "Environment must not be null"); + this.source = source; + this.environment = environment; + } + + public String toString() { + return "Wise environment: '" + environment + "', source: '" + source + "'"; + } + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsPropertiesPropertySource.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsPropertiesPropertySource.java deleted file mode 100644 index a9bf574..0000000 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsPropertiesPropertySource.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.wise.common.environment; - -import java.util.Map; -import java.util.Properties; -import org.springframework.core.env.MapPropertySource; -import org.springframework.lang.Nullable; - -public class WiseDefaultsPropertiesPropertySource extends MapPropertySource { - - @SuppressWarnings({"rawtypes", "unchecked"}) - public WiseDefaultsPropertiesPropertySource(String name, Properties source) { - super(name, (Map) source); - } - - protected WiseDefaultsPropertiesPropertySource(String name, Map source) { - super(name, source); - } - - @Override - @Nullable - public Object getProperty(String name) { - return this.source.get("wise.defaults." + name); - } - -} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsSystemEnvironmentPropertySource.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsSystemEnvironmentPropertySource.java deleted file mode 100644 index 1674f5d..0000000 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseDefaultsSystemEnvironmentPropertySource.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.wise.common.environment; - -import java.util.Map; -import org.springframework.core.env.MapPropertySource; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -public class WiseDefaultsSystemEnvironmentPropertySource extends MapPropertySource { - - public WiseDefaultsSystemEnvironmentPropertySource(String name, Map source) { - super(name, source); - } - - @Override - public boolean containsProperty(String name) { - return (getProperty(name) != null); - } - - @Override - @Nullable - public Object getProperty(String name) { - String actualName = resolvePropertyName(name); - if (logger.isDebugEnabled() && !name.equals(actualName)) { - logger.debug("PropertySource '" + getName() + "' does not contain property '" + name + - "', but found equivalent '" + actualName + "'"); - } - return super.getProperty(actualName); - } - - /** - * Check to see if this property source contains a property with the given name, or any underscore / uppercase variation thereof. Return the - * resolved name if one is found or otherwise the original name. Never returns {@code null}. - */ - protected final String resolvePropertyName(String name) { - var nameWithDefaultPrefix = "WISE_DEFAULTS_" + name; - - Assert.notNull(name, "Property name must not be null"); - String resolvedName = checkPropertyName(nameWithDefaultPrefix); - if (resolvedName != null) { - return resolvedName; - } - String uppercasedName = nameWithDefaultPrefix.toUpperCase(); - if (!name.equals(uppercasedName)) { - resolvedName = checkPropertyName(uppercasedName); - if (resolvedName != null) { - return resolvedName; - } - } - return name; - } - - @Nullable - private String checkPropertyName(String name) { - // Check name as-is - if (containsKey(name)) { - return name; - } - // Check name with just dots replaced - String noDotName = name.replace('.', '_'); - if (!name.equals(noDotName) && containsKey(noDotName)) { - return noDotName; - } - // Check name with just hyphens replaced - String noHyphenName = name.replace('-', '_'); - if (!name.equals(noHyphenName) && containsKey(noHyphenName)) { - return noHyphenName; - } - // Check name with dots and hyphens replaced - String noDotNoHyphenName = noDotName.replace('-', '_'); - if (!noDotName.equals(noDotNoHyphenName) && containsKey(noDotNoHyphenName)) { - return noDotNoHyphenName; - } - // Give up - return null; - } - - private boolean containsKey(String name) { - return this.source.containsKey(name); - } - -} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironment.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironment.java index b5f03fe..6e42d55 100644 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironment.java +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironment.java @@ -1,15 +1,137 @@ package com.wise.common.environment; -public interface WiseEnvironment { - boolean isProduction(); +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; - boolean isStaging(); +@Slf4j +public enum WiseEnvironment { + WISE, - boolean isIntegrationTest(); + PRODUCTION(WISE), + PCI_PLASTIC_PRODUCTION(PRODUCTION), - boolean isDevelopment(); + STAGING(WISE), + CUSTOM_ENVIRONMENT(STAGING), + SANDBOX(STAGING), + PCI_PLASTIC_STAGING(STAGING), - boolean isSandbox(); + TEST(WISE), + INTEGRATION_TEST(TEST), + UNIT_TEST(TEST), - boolean isCustomEnvironment(); + DEVELOPMENT(WISE), + + DEVELOPMENT_AGAINST_CUSTOM_ENVIRONMENT(DEVELOPMENT); + + private WiseEnvironment parent; + + WiseEnvironment() { + } + + WiseEnvironment(WiseEnvironment parent) { + this.parent = parent; + } + + public WiseEnvironment parent() { + return parent; + } + + private static final Map NAME_INDEX = + Arrays.stream(WiseEnvironment.values()).collect(Collectors.toMap(we -> we.name().toLowerCase(), we -> we)); + + public static WiseEnvironment getByName(String name) { + return StringUtils.isAllLowerCase(name) ? NAME_INDEX.get(name) : NAME_INDEX.get(name.toLowerCase()); + } + + private static final Map> defaultProperties = new ConcurrentHashMap<>(); + + private static final Map, String[]> defaultPropertyNamesCache = new ConcurrentHashMap<>(); + + private static WiseEnvironmentProvider wiseEnvironmentProvider; + + public static void init(List activeEnvironments) { + wiseEnvironmentProvider = new CachingWiseEnvironmentProvider(activeEnvironments); + } + + public static void setDefaultProperty(String source, String name, Object value) { + setDefaultProperty(source, WiseEnvironment.WISE, name, value); + } + + public static void setDefaultProperty(String source, WiseEnvironment wiseEnvironment, String name, Object value) { + defaultProperties + .computeIfAbsent(wiseEnvironment, k -> new ConcurrentHashMap<>()) + .put(name, new PropertyContainer(source, wiseEnvironment, value)); + + defaultPropertyNamesCache.clear(); + } + + public static List getActiveEnvironments() { + return wiseEnvironmentProvider.getActiveEnvironments(); + } + + public static boolean isEnvironmentActive(WiseEnvironment environment) { + return wiseEnvironmentProvider.isEnvironmentActive(environment); + } + + static PropertyContainer getDefaultPropertyContainer(String name) { + var activeEnvironments = getActiveEnvironments(); + + if (activeEnvironments != null) { + for (var activeEnvironment : activeEnvironments) { + while (activeEnvironment != null) { + var envProps = WiseEnvironment.defaultProperties.get(activeEnvironment); + if (envProps != null) { + var value = envProps.get(name); + if (value != null) { + return value; + } + } + + activeEnvironment = activeEnvironment.parent(); + } + } + } + + return null; + } + + public static Object getDefaultProperty(String name) { + var container = getDefaultPropertyContainer(name); + return container == null ? null : container.getValue(); + } + + public static String[] getDefaultPropertyNames() { + var activeEnvironments = getActiveEnvironments(); + + if (activeEnvironments == null) { + return new String[0]; + } + + return defaultPropertyNamesCache.computeIfAbsent(activeEnvironments, k -> { + var result = new HashSet(); + + for (var activeEnvironment : activeEnvironments) { + while (activeEnvironment != null) { + var envProps = WiseEnvironment.defaultProperties.get(activeEnvironment); + if (envProps != null) { + result.addAll(envProps.keySet()); + } + + activeEnvironment = activeEnvironment.parent(); + } + } + return result.toArray(new String[0]); + }); + } + + public static void setDefaultProperties(Consumer dslConsumer) { + dslConsumer.accept(new DefaultPropertiesSetterDsl()); + } } diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentAutoConfiguration.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentAutoConfiguration.java deleted file mode 100644 index bee7a62..0000000 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentAutoConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.wise.common.environment; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; - -@AutoConfiguration -public class WiseEnvironmentAutoConfiguration { - - @Bean - @ConditionalOnMissingBean(WiseEnvironmentProperties.class) - @ConfigurationProperties(prefix = "wise.environment.core", ignoreUnknownFields = false) - public WiseEnvironmentProperties wiseEnvironmentProperties() { - return new WiseEnvironmentProperties(); - } - - @Bean - @ConditionalOnMissingBean(WiseEnvironment.class) - public DefaultWiseEnvironment wiseEnvironment(WiseEnvironmentProperties wiseEnvironmentProperties) { - return new DefaultWiseEnvironment(wiseEnvironmentProperties); - } -} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDefaultsPropertySource.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDefaultsPropertySource.java new file mode 100644 index 0000000..2b34b8a --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDefaultsPropertySource.java @@ -0,0 +1,39 @@ +package com.wise.common.environment; + +import com.wise.common.environment.WiseEnvironmentDefaultsPropertySource.WiseEnvironmentDefaultsPropertySourceSource; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.EnumerablePropertySource; + +public class WiseEnvironmentDefaultsPropertySource extends EnumerablePropertySource implements + OriginLookup { + + public WiseEnvironmentDefaultsPropertySource() { + super("WiseEnvironmentAwareDefaultsPropertySource", new WiseEnvironmentDefaultsPropertySourceSource()); + } + + @Override + public Object getProperty(String name) { + return WiseEnvironment.getDefaultProperty(name); + } + + @Override + public String[] getPropertyNames() { + return WiseEnvironment.getDefaultPropertyNames(); + } + + @Override + public Origin getOrigin(String name) { + var container = WiseEnvironment.getDefaultPropertyContainer(name); + return container == null ? null : container.getOrigin(); + } + + static class WiseEnvironmentDefaultsPropertySourceSource implements OriginLookup { + + @Override + public Origin getOrigin(String name) { + var container = WiseEnvironment.getDefaultPropertyContainer(name); + return container == null ? null : container.getOrigin(); + } + } +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDetector.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDetector.java new file mode 100644 index 0000000..c72534d --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentDetector.java @@ -0,0 +1,8 @@ +package com.wise.common.environment; + +import java.util.List; + +public interface WiseEnvironmentDetector { + + List detect(); +} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentEnvironmentPostProcessor.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentEnvironmentPostProcessor.java index 8146f0f..f7d62c3 100644 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentEnvironmentPostProcessor.java +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentEnvironmentPostProcessor.java @@ -1,21 +1,29 @@ package com.wise.common.environment; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; @Slf4j +@Order public class WiseEnvironmentEnvironmentPostProcessor implements EnvironmentPostProcessor { - private static final String SYSTEM_ENV_PROPERTY_SOURCE_KEY = WiseEnvironmentEnvironmentPostProcessor.class.getName() + ".systemEnv"; - private static final String SYSTEM_PROPS_PROPERTY_SOURCE_KEY = WiseEnvironmentEnvironmentPostProcessor.class.getName() + ".systemProps"; - @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - environment.getPropertySources().addLast( - new WiseDefaultsPropertiesPropertySource(SYSTEM_PROPS_PROPERTY_SOURCE_KEY, environment.getSystemProperties())); - environment.getPropertySources().addLast( - new WiseDefaultsSystemEnvironmentPropertySource(SYSTEM_ENV_PROPERTY_SOURCE_KEY, environment.getSystemEnvironment())); + var wiseEnvironmentDetector = new DefaultWiseEnvironmentDetector(environment.getProperty("wise.environments.active")); + final var activeEnvironments = wiseEnvironmentDetector.detect(); + + if (activeEnvironments.isEmpty()) { + log.info("No active Wise environments detected."); + } else { + log.info("Active Wise environments detected are '{}'.", activeEnvironments.stream().map(Enum::name).collect(Collectors.joining(","))); + } + + WiseEnvironment.init(activeEnvironments); + + environment.getPropertySources().addLast(new WiseEnvironmentDefaultsPropertySource()); } } diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProperties.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProperties.java deleted file mode 100644 index d5c789b..0000000 --- a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProperties.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.wise.common.environment; - -import lombok.Data; -import lombok.experimental.Accessors; - -@Data -@Accessors(chain = true) -public class WiseEnvironmentProperties { - - private Type type; - - private SubType subType; - - public enum Type { - PRODUCTION, - STAGING, - INTEGRATION_TEST, - DEVELOPMENT - } - - public enum SubType { - SANDBOX, - CUSTOM_ENVS - } -} diff --git a/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProvider.java b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProvider.java new file mode 100644 index 0000000..d305e53 --- /dev/null +++ b/wise-environment-starter/src/main/java/com/wise/common/environment/WiseEnvironmentProvider.java @@ -0,0 +1,10 @@ +package com.wise.common.environment; + +import java.util.List; + +public interface WiseEnvironmentProvider { + + List getActiveEnvironments(); + + boolean isEnvironmentActive(WiseEnvironment environment); +} diff --git a/wise-environment-starter/src/main/resources/META-INF/spring.factories b/wise-environment-starter/src/main/resources/META-INF/spring.factories index 2596f7c..b91ba0a 100644 --- a/wise-environment-starter/src/main/resources/META-INF/spring.factories +++ b/wise-environment-starter/src/main/resources/META-INF/spring.factories @@ -1,2 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wise.common.environment.WiseEnvironmentAutoConfiguration org.springframework.boot.env.EnvironmentPostProcessor=com.wise.common.environment.WiseEnvironmentEnvironmentPostProcessor \ No newline at end of file diff --git a/wise-environment-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/wise-environment-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 42fdf88..0000000 --- a/wise-environment-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -com.wise.common.environment.WiseEnvironmentAutoConfiguration \ No newline at end of file diff --git a/wise-environment-starter/src/test/java/com/wise/common/environment/Application1IntTest.java b/wise-environment-starter/src/test/java/com/wise/common/environment/Application1IntTest.java new file mode 100644 index 0000000..c7e427e --- /dev/null +++ b/wise-environment-starter/src/test/java/com/wise/common/environment/Application1IntTest.java @@ -0,0 +1,19 @@ +package com.wise.common.environment; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = TestApplication.class) +@ActiveProfiles("application1") +public class Application1IntTest { + + @Test + void applicationIsConfigured() { + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.PCI_PLASTIC_STAGING)); + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.STAGING)); + } + +} diff --git a/wise-environment-starter/src/test/java/com/wise/common/environment/Application2IntTest.java b/wise-environment-starter/src/test/java/com/wise/common/environment/Application2IntTest.java new file mode 100644 index 0000000..c12e920 --- /dev/null +++ b/wise-environment-starter/src/test/java/com/wise/common/environment/Application2IntTest.java @@ -0,0 +1,22 @@ +package com.wise.common.environment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = TestApplication.class) +@ActiveProfiles("application2") +public class Application2IntTest { + + @Test + void applicationIsConfigured() { + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.PCI_PLASTIC_STAGING)); + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.STAGING)); + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.DEVELOPMENT)); + assertFalse(WiseEnvironment.isEnvironmentActive(WiseEnvironment.DEVELOPMENT_AGAINST_CUSTOM_ENVIRONMENT)); + } + +} diff --git a/wise-environment-starter/src/test/java/com/wise/common/environment/ApplicationIntTest.java b/wise-environment-starter/src/test/java/com/wise/common/environment/ApplicationIntTest.java index b2d84fb..92bfa2e 100644 --- a/wise-environment-starter/src/test/java/com/wise/common/environment/ApplicationIntTest.java +++ b/wise-environment-starter/src/test/java/com/wise/common/environment/ApplicationIntTest.java @@ -1,35 +1,53 @@ package com.wise.common.environment; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; @SpringBootTest(classes = TestApplication.class) public class ApplicationIntTest { @Autowired - private WiseEnvironment wiseEnvironment; - - @Autowired - private TestProperties testProperties; + private Environment environment; @Test void applicationIsConfigured() { - assertTrue(wiseEnvironment.isIntegrationTest()); + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.TEST)); + assertTrue(WiseEnvironment.isEnvironmentActive(WiseEnvironment.INTEGRATION_TEST)); + assertFalse(WiseEnvironment.isEnvironmentActive(WiseEnvironment.UNIT_TEST)); } @Test - void defaultEnvVariablesAreApplied() { - // Default set via ENV can be overridden via application.yml - assertEquals("blah", testProperties.getValue1()); - // Default set via ENV is applied - assertEquals("foo", testProperties.getValue2()); - // Environment variables still overwrite everything - assertEquals("foo", testProperties.getValue3()); - // Defaults can be used through system properties as well - assertEquals("foo", testProperties.getValue4()); + void defaultsCanBeSet() { + WiseEnvironment.setDefaultProperty("mySource", "mykey", "robot"); + + WiseEnvironment.setDefaultProperty("mySource", WiseEnvironment.TEST, "mykey", "cat"); + assertThat(environment.getProperty("mykey"), equalTo("cat")); + assertThat(WiseEnvironment.getDefaultPropertyContainer("mykey").getOrigin().getEnvironment(), equalTo(WiseEnvironment.TEST)); + + WiseEnvironment.setDefaultProperty("mySource", WiseEnvironment.INTEGRATION_TEST, "mykey", "dog"); + assertThat(environment.getProperty("mykey"), equalTo("dog")); + + assertThat(WiseEnvironment.getDefaultPropertyContainer("mykey").getOrigin().getEnvironment(), equalTo(WiseEnvironment.INTEGRATION_TEST)); + + WiseEnvironment.setDefaultProperties(dsl -> dsl + .source("mySource") + .environment(WiseEnvironment.UNIT_TEST) + .keyPrefix("prefix.") + .set("myotherkey", "horse") + .environment(WiseEnvironment.TEST) + .set("myotherkey", "mouse") + .keyPrefix(null) + .set("onemorekey", "moose") + ); + assertThat(environment.getProperty("prefix.myotherkey"), equalTo("mouse")); + assertThat(environment.getProperty("onemorekey"), equalTo("moose")); + } } diff --git a/wise-environment-starter/src/test/java/com/wise/common/environment/TestApplication.java b/wise-environment-starter/src/test/java/com/wise/common/environment/TestApplication.java index e05c1ba..9029d74 100644 --- a/wise-environment-starter/src/test/java/com/wise/common/environment/TestApplication.java +++ b/wise-environment-starter/src/test/java/com/wise/common/environment/TestApplication.java @@ -1,15 +1,8 @@ package com.wise.common.environment; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; @SpringBootApplication public class TestApplication { - @Bean - @ConfigurationProperties("wise.environment.test") - public TestProperties testProperties() { - return new TestProperties(); - } } diff --git a/wise-environment-starter/src/test/resources/application.yml b/wise-environment-starter/src/test/resources/application.yml index 0771846..ab7756c 100644 --- a/wise-environment-starter/src/test/resources/application.yml +++ b/wise-environment-starter/src/test/resources/application.yml @@ -1,7 +1,15 @@ -wise: - environment: - core: - type: integration_test - test: - value1: blah - value3: blah \ No newline at end of file +spring: + config: + activate: + on-profile: application1 + +wise.environments.active: pci_plastic_staging + +--- + +spring: + config: + activate: + on-profile: application2 + +wise.environments.active: development, staging, pci_plastic_staging \ No newline at end of file