diff --git a/base/src/main/java/org/killbill/billing/platform/config/DefaultKillbillConfigSource.java b/base/src/main/java/org/killbill/billing/platform/config/DefaultKillbillConfigSource.java index e6ae6c97f..133f36f5c 100644 --- a/base/src/main/java/org/killbill/billing/platform/config/DefaultKillbillConfigSource.java +++ b/base/src/main/java/org/killbill/billing/platform/config/DefaultKillbillConfigSource.java @@ -21,11 +21,11 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,6 +33,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.Set; import java.util.TimeZone; import javax.annotation.Nullable; @@ -73,9 +74,15 @@ public class DefaultKillbillConfigSource implements KillbillConfigSource, OSGICo private static volatile int GMT_WARNING = NOT_SHOWN; private static volatile int ENTROPY_WARNING = NOT_SHOWN; + private static final List HIGH_TO_LOW_PRIORITY_ORDER = + Collections.unmodifiableList(Arrays.asList("ImmutableSystemProperties", + "EnvironmentVariables", + "RuntimeConfiguration", + "KillBillDefaults")); + private final PropertiesWithSourceCollector propertiesCollector; - private final Properties properties; + private volatile Map> cachedPropertiesBySource; public DefaultKillbillConfigSource() throws IOException, URISyntaxException { this((String) null); @@ -93,26 +100,16 @@ public DefaultKillbillConfigSource(@Nullable final String file, final Map propsMap = propertiesToMap(properties); - propertiesCollector.addProperties(category, propsMap); + final Properties properties = new Properties(); + properties.load(UriAccessor.accessUri(Objects.requireNonNull(this.getClass().getResource(file)).toURI())); + final Map propsMap = propertiesToMap(properties); + propertiesCollector.addProperties("RuntimeConfiguration", propsMap); } - for (final Entry entry : extraDefaultProperties.entrySet()) { - if (entry.getValue() != null) { - properties.put(entry.getKey(), entry.getValue()); - } - } - - propertiesCollector.addProperties("ExtraDefaultProperties", extraDefaultProperties); - - populateDefaultProperties(); + populateDefaultProperties(extraDefaultProperties); if (Boolean.parseBoolean(getString(LOOKUP_ENVIRONMENT_VARIABLES))) { overrideWithEnvironmentVariables(); @@ -125,32 +122,148 @@ public DefaultKillbillConfigSource(@Nullable final String file, final Map> bySource = getPropertiesBySource(); + + for (final Map sourceProps : bySource.values()) { + final String value = sourceProps.get(propertyName); + if (value != null) { + return value; + } + } + + return null; } @Override public Properties getProperties() { final Properties result = new Properties(); - // using properties.stringPropertyNames() because `result.putAll(properties)` not working when running inside - // tomcat, if we put configuration in tomcat's catalina.properties - // See: - // - https://github.com/killbill/technical-support/issues/61 - // - https://github.com/killbill/technical-support/issues/67 - // - // We have TestDefaultKillbillConfigSource#testGetProperties() that cover this, but seems like this is similar - // to one of our chicken-egg problem? (see loadPropertiesFromFileOrSystemProperties() below) - properties.stringPropertyNames().forEach(key -> result.setProperty(key, properties.getProperty(key))); + + getPropertiesBySource().forEach((source, props) -> props.forEach(result::setProperty)); + + return result; + } + + @Override + public Map> getPropertiesBySource() { + if (cachedPropertiesBySource == null) { + synchronized (lock) { + if (cachedPropertiesBySource == null) { + rebuildCache(); + } + } + } + + return Collections.unmodifiableMap(cachedPropertiesBySource); + } + + protected void rebuildCache() { + cachedPropertiesBySource = computePropertiesBySource(); + } + + private void invalidateCache() { + synchronized (lock) { + cachedPropertiesBySource = null; + } + } + + private Map> computePropertiesBySource() { + final Map> runtimeBySource = RuntimeConfigRegistry.getAllBySource(); + runtimeBySource.forEach((source, props) -> { + if (!props.isEmpty()) { + propertiesCollector.addProperties(source, props); + } + }); + + final Map> collectorBySource = propertiesCollector.getPropertiesBySource(); + + final Map> propertyToSources = new HashMap<>(); + collectorBySource.forEach((source, properties) -> { + properties.forEach(property -> { + propertyToSources.computeIfAbsent(property.getKey(), k -> new ArrayList<>()).add(source); + }); + }); + + final Set warnedConflicts = new HashSet<>(); + final Map> result = new LinkedHashMap<>(); + + final Set processedProperties = new HashSet<>(); + + for (final String source : HIGH_TO_LOW_PRIORITY_ORDER) { + final List properties = collectorBySource.get(source); + if (properties == null || properties.isEmpty()) { + continue; + } + + final Map sourceMap = new LinkedHashMap<>(); + + for (final PropertyWithSource prop : properties) { + final String propertyKey = prop.getKey(); + final String propertyValue = prop.getValue(); + + if (propertyValue == null) { + continue; + } + + if (!processedProperties.contains(propertyKey)) { + sourceMap.put(propertyKey, propertyValue); + processedProperties.add(propertyKey); + + final List sources = propertyToSources.get(propertyKey); + if (sources != null && sources.size() > 1 && !warnedConflicts.contains(propertyKey)) { + if (shouldWarnAboutConflict(sources)) { + warnedConflicts.add(propertyKey); + logger.warn("Property conflict detected for '{}': defined in sources {} - using value from '{}': '{}'", + propertyKey, sources, source, propertyValue); + } + } + } + } + + if (!sourceMap.isEmpty()) { + result.put(source, Collections.unmodifiableMap(sourceMap)); + } + } + + collectorBySource.forEach((source, properties) -> { + if (HIGH_TO_LOW_PRIORITY_ORDER.contains(source)) { + return; + } + + final Map sourceMap = new LinkedHashMap<>(); + for (final PropertyWithSource prop : properties) { + final String propertyKey = prop.getKey(); + final String propertyValue = prop.getValue(); + + if (propertyValue == null) { + continue; + } + + if (!processedProperties.contains(propertyKey)) { + sourceMap.put(propertyKey, propertyValue); + processedProperties.add(propertyKey); + } + } + + if (!sourceMap.isEmpty()) { + result.put(source, Collections.unmodifiableMap(sourceMap)); + } + }); RuntimeConfigRegistry.getAll().forEach((key, value) -> { - if (!result.containsKey(key)) { - result.setProperty(key, value); + if (!processedProperties.contains(key)) { + result.computeIfAbsent("RuntimeConfigRegistry", k -> new LinkedHashMap<>()) + .put(key, value); } }); - return result; + return Collections.unmodifiableMap(result); } - private Properties loadPropertiesFromFileOrSystemProperties() { + private void loadPropertiesFromFileOrSystemProperties() { // Chicken-egg problem. It would be nice to have the property in e.g. KillbillServerConfig, // but we need to build the ConfigSource first... final String propertiesFileLocation = System.getProperty(PROPERTIES_FILE); @@ -160,11 +273,10 @@ private Properties loadPropertiesFromFileOrSystemProperties() { final Properties properties = new Properties(); properties.load(UriAccessor.accessUri(propertiesFileLocation)); - final String category = extractFileNameFromPath(propertiesFileLocation); final Map propsMap = propertiesToMap(properties); - propertiesCollector.addProperties(category, propsMap); + propertiesCollector.addProperties("RuntimeConfiguration", propsMap); - return properties; + return; } catch (final IOException e) { logger.warn("Unable to access properties file, defaulting to system properties", e); } catch (final URISyntaxException e) { @@ -172,21 +284,25 @@ private Properties loadPropertiesFromFileOrSystemProperties() { } } - propertiesCollector.addProperties("SystemProperties", propertiesToMap(System.getProperties())); - - return new Properties(System.getProperties()); + propertiesCollector.addProperties("RuntimeConfiguration", propertiesToMap(System.getProperties())); } @VisibleForTesting - protected void populateDefaultProperties() { + protected void populateDefaultProperties(final Map extraDefaultProperties) { final Properties defaultProperties = getDefaultProperties(); + defaultProperties.putAll(extraDefaultProperties); + + final Map defaultsToAdd = new HashMap<>(); + for (final String propertyName : defaultProperties.stringPropertyNames()) { // Let the user override these properties - if (properties.get(propertyName) == null) { - properties.put(propertyName, defaultProperties.get(propertyName)); + if (!hasProperty(propertyName)) { + defaultsToAdd.put(propertyName, defaultProperties.getProperty(propertyName)); } } + final Map immutableProps = new HashMap<>(); + final Properties defaultSystemProperties = getDefaultSystemProperties(); for (final String propertyName : defaultSystemProperties.stringPropertyNames()) { @@ -212,6 +328,9 @@ protected void populateDefaultProperties() { // System.setProperty(propertyName, GMT_ID); TimeZone.setDefault(TimeZone.getTimeZone(GMT_ID)); + + immutableProps.put(PROP_USER_TIME_ZONE, GMT_ID); + continue; } @@ -219,6 +338,10 @@ protected void populateDefaultProperties() { if (System.getProperty(propertyName) == null) { System.setProperty(propertyName, defaultSystemProperties.get(propertyName).toString()); } + + if (!hasProperty(propertyName)) { + defaultsToAdd.put(propertyName, defaultSystemProperties.getProperty(propertyName)); + } } // WARN for missing PROP_SECURITY_EGD @@ -233,48 +356,28 @@ protected void populateDefaultProperties() { } } - defaultSystemProperties.putAll(defaultProperties); + if (!immutableProps.isEmpty()) { + propertiesCollector.addProperties("ImmutableSystemProperties", immutableProps); + } - final Map propsMap = propertiesToMap(defaultSystemProperties); - propertiesCollector.addProperties("DefaultSystemProperties", propsMap); + if (!defaultsToAdd.isEmpty()) { + propertiesCollector.addProperties("KillBillDefaults", defaultsToAdd); + } } - @Override - public Map> getPropertiesBySource() { - final Map currentProps = new HashMap<>(); - properties.stringPropertyNames().forEach(key -> currentProps.put(key, properties.getProperty(key))); - - final Map> runtimeBySource = RuntimeConfigRegistry.getAllBySource(); - runtimeBySource.forEach((source, props) -> { - final Map filteredProps = new HashMap<>(); - props.forEach((key, value) -> { - if (!currentProps.containsKey(key)) { - filteredProps.put(key, value); - } - }); - if (!filteredProps.isEmpty()) { - propertiesCollector.addProperties(source, filteredProps); - } - }); - - final Map> propertiesBySource = propertiesCollector.getPropertiesBySource(); - - final Map> result = new LinkedHashMap<>(); - - propertiesBySource.forEach((source, properties) -> { - final Map sourceProperties = new LinkedHashMap<>(); - properties.forEach(prop -> { - sourceProperties.put(prop.getKey(), prop.getValue()); - }); - result.put(source, Collections.unmodifiableMap(sourceProperties)); - }); - - return Collections.unmodifiableMap(result); + private boolean hasProperty(final String propertyName) { + return propertiesCollector.getAllProperties().stream() + .anyMatch(p -> p.getKey().equals(propertyName)); } @VisibleForTesting public void setProperty(final String propertyName, final Object propertyValue) { - properties.put(propertyName, propertyValue); + final Map override = new HashMap<>(); + override.put(propertyName, String.valueOf(propertyValue)); + propertiesCollector.addProperties("RuntimeConfiguration", override); + + invalidateCache(); + rebuildCache(); } @VisibleForTesting @@ -284,6 +387,7 @@ protected Properties getDefaultProperties() { properties.put("org.killbill.persistent.bus.external.historyTableName", "bus_ext_events_history"); properties.put(ENABLE_JASYPT_DECRYPTION, "false"); properties.put(LOOKUP_ENVIRONMENT_VARIABLES, "true"); + return properties; } @@ -304,8 +408,7 @@ protected Properties getDefaultSystemProperties() { private void overrideWithEnvironmentVariables() { // Find all Kill Bill properties in the environment variables - final Map env = System.getenv(); - + final Map env = getEnvironmentVariables(); final Map kbEnvVariables = new HashMap<>(); for (final Entry entry : env.entrySet()) { @@ -317,12 +420,16 @@ private void overrideWithEnvironmentVariables() { final String value = entry.getValue(); kbEnvVariables.put(propertyName, value); - properties.setProperty(propertyName, value); } propertiesCollector.addProperties("EnvironmentVariables", kbEnvVariables); } + @VisibleForTesting + protected Map getEnvironmentVariables() { + return System.getenv(); + } + public List getAllPropertiesWithSource() { return propertiesCollector.getAllProperties(); } @@ -332,19 +439,58 @@ String fromEnvVariableName(final String key) { return key.replace(ENVIRONMENT_VARIABLE_PREFIX, "").replaceAll("_", "\\."); } + private String getPropertyDirect(final String propertyName) { + final Map> collectorBySource = propertiesCollector.getPropertiesBySource(); + + for (final String source : HIGH_TO_LOW_PRIORITY_ORDER) { + final List properties = collectorBySource.get(source); + if (properties != null) { + for (final PropertyWithSource prop : properties) { + if (prop.getKey().equals(propertyName)) { + return prop.getValue(); + } + } + } + } + + for (final Map.Entry> entry : collectorBySource.entrySet()) { + if (!HIGH_TO_LOW_PRIORITY_ORDER.contains(entry.getKey())) { + for (final PropertyWithSource prop : entry.getValue()) { + if (prop.getKey().equals(propertyName)) { + return prop.getValue(); + } + } + } + } + + return null; + } + private void decryptJasyptProperties() { final String password = getEnvironmentVariable(JASYPT_ENCRYPTOR_PASSWORD_KEY, System.getProperty(JASYPT_ENCRYPTOR_PASSWORD_KEY)); final String algorithm = getEnvironmentVariable(JASYPT_ENCRYPTOR_ALGORITHM_KEY, System.getProperty(JASYPT_ENCRYPTOR_ALGORITHM_KEY)); - final Enumeration keys = properties.keys(); + final Map> decryptedBySource = new HashMap<>(); + final StandardPBEStringEncryptor encryptor = initializeEncryptor(password, algorithm); // Iterate over all properties and decrypt ones that match - while (keys.hasMoreElements()) { - final String key = (String) keys.nextElement(); - final String value = (String) properties.get(key); + final List allProperties = propertiesCollector.getAllProperties(); + for (final PropertyWithSource prop : allProperties) { + final String key = prop.getKey(); + final String value = prop.getValue(); final Optional decryptableValue = decryptableValue(value); - decryptableValue.ifPresent(s -> properties.setProperty(key, encryptor.decrypt(s))); + if (decryptableValue.isPresent()) { + final String decryptedValue = encryptor.decrypt(decryptableValue.get()); + + final String source = prop.getSource(); + if (source != null) { + decryptedBySource.computeIfAbsent(source, k -> new HashMap<>()) + .put(key, decryptedValue); + } + } } + + decryptedBySource.forEach(propertiesCollector::addProperties); } private StandardPBEStringEncryptor initializeEncryptor(final String password, final String algorithm) { @@ -386,29 +532,17 @@ private Optional decryptableValue(final String value) { return Optional.empty(); } - private String extractFileNameFromPath(String path) { - if (path == null || path.isEmpty()) { - return "unknown.properties"; - } - - if (path.startsWith("file://")) { - path = path.substring("file://".length()); - } - - final Path fileName = Paths.get(path).getFileName(); - if (fileName == null) { - return "unknown.properties"; - } - - return fileName.toString(); - } - private Map propertiesToMap(final Properties props) { final Map propertiesMap = new HashMap<>(); for (final Map.Entry entry : props.entrySet()) { propertiesMap.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); } - return propertiesMap; } + + private boolean shouldWarnAboutConflict(final List sources) { + return sources != null && + sources.contains("EnvironmentVariables") && + sources.contains("RuntimeConfiguration"); + } } diff --git a/base/src/main/java/org/killbill/billing/platform/config/PropertiesWithSourceCollector.java b/base/src/main/java/org/killbill/billing/platform/config/PropertiesWithSourceCollector.java index d7d1b4625..8bbbbde2f 100644 --- a/base/src/main/java/org/killbill/billing/platform/config/PropertiesWithSourceCollector.java +++ b/base/src/main/java/org/killbill/billing/platform/config/PropertiesWithSourceCollector.java @@ -22,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class PropertiesWithSourceCollector { @@ -29,12 +30,20 @@ public class PropertiesWithSourceCollector { private volatile List properties = new ArrayList<>(); private final Object lock = new Object(); - public void addProperties(String source, Map props) { + public void addProperties(final String source, final Map props) { synchronized (lock) { - List newList = new ArrayList<>(properties); - props.forEach((key, value) -> - newList.add(new PropertyWithSource(source, key, value))); - this.properties = Collections.unmodifiableList(newList); + final List updatedProperties = new ArrayList<>(properties); + + final Set keysToAdd = props.keySet(); + updatedProperties.removeIf(property -> property.getSource().equals(source) && keysToAdd.contains(property.getKey())); + + props.forEach((key, value) -> { + if (value != null) { + updatedProperties.add(new PropertyWithSource(source, key, value)); + } + }); + + this.properties = Collections.unmodifiableList(updatedProperties); } } diff --git a/base/src/main/java/org/killbill/billing/platform/jndi/JNDIManager.java b/base/src/main/java/org/killbill/billing/platform/jndi/JNDIManager.java index 7c044fccd..d4bcb68a7 100644 --- a/base/src/main/java/org/killbill/billing/platform/jndi/JNDIManager.java +++ b/base/src/main/java/org/killbill/billing/platform/jndi/JNDIManager.java @@ -31,6 +31,7 @@ import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; +import javax.naming.spi.NamingManager; import org.killbill.commons.utils.Preconditions; import org.slf4j.Logger; @@ -92,7 +93,17 @@ public Object lookup(final String name) { try { context = getContext(); - return context.lookup(name); + final Object obj = context.lookup(name); + + if (obj instanceof Reference) { + try { + return NamingManager.getObjectInstance(obj, null, null, null); + } catch (final Exception e) { + logger.warn("Failed to dereference JNDI Reference for {}, returning Reference object", name, e); + } + } + + return obj; } catch (final NamingException e) { logger.warn("Error looking up " + name, e); } finally { diff --git a/base/src/test/java/org/killbill/billing/platform/config/TestDefaultKillbillConfigSource.java b/base/src/test/java/org/killbill/billing/platform/config/TestDefaultKillbillConfigSource.java index 8e6191a08..40a01ed7a 100644 --- a/base/src/test/java/org/killbill/billing/platform/config/TestDefaultKillbillConfigSource.java +++ b/base/src/test/java/org/killbill/billing/platform/config/TestDefaultKillbillConfigSource.java @@ -21,8 +21,12 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Properties; import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; import org.jasypt.exceptions.EncryptionOperationNotPossibleException; @@ -55,6 +59,101 @@ public void setup() { System.clearProperty(ENCRYPTED_PROPERTY_2); } + @Test + public void testGetPropertiesBySourceContainsExpectedSources() throws URISyntaxException, IOException { + final Map runtimeConfig = new HashMap<>(); + runtimeConfig.put("org.killbill.dao.user", "root"); + + final OSGIConfigProperties configSource = new DefaultKillbillConfigSource(null, runtimeConfig) { + @Override + protected Map getEnvironmentVariables() { + final Map mockEnv = new HashMap<>(); + mockEnv.put(ENVIRONMENT_VARIABLE_PREFIX + "org_killbill_dao_user", "root"); + return mockEnv; + } + }; + + final Map> propsBySource = configSource.getPropertiesBySource(); + + Assert.assertTrue(propsBySource.containsKey("ImmutableSystemProperties")); + Assert.assertTrue(propsBySource.containsKey("EnvironmentVariables")); + Assert.assertTrue(propsBySource.containsKey("RuntimeConfiguration")); + Assert.assertTrue(propsBySource.containsKey("KillBillDefaults")); + } + + @Test(groups = "fast") + public void testGetPropertiesAndGetPropertiesBySourceAreInSync() throws URISyntaxException, IOException { + // RuntimeConfiguration + System.setProperty("org.killbill.dao.user", "root"); + System.setProperty("org.killbill.dao.password", "password"); + + // KillBillDefaults + final Map killbillDefaultConfig = new HashMap<>(); + killbillDefaultConfig.put("org.killbill.server.shutdownDelay", "3s"); + killbillDefaultConfig.put("org.killbill.billing.osgi.dao.logLevel", "INFO"); + + // ImmutableSystemProperties + killbillDefaultConfig.put("user.timezone", "GMT"); + + // EnvironmentVariables + final OSGIConfigProperties configSource = new DefaultKillbillConfigSource(null, killbillDefaultConfig) { + @Override + protected Map getEnvironmentVariables() { + final Map mockEnv = new HashMap<>(); + mockEnv.put(ENVIRONMENT_VARIABLE_PREFIX + "org_killbill_dao_healthCheckConnectionTimeout", "11s"); + return mockEnv; + } + }; + + final Properties mergedProperties = configSource.getProperties(); + final Map> propertiesBySource = configSource.getPropertiesBySource(); + + final Map allProperties = new HashMap<>(); + propertiesBySource.forEach((source, props) -> allProperties.putAll(props)); + + for (final String key : mergedProperties.stringPropertyNames()) { + final String valueFromFlat = mergedProperties.getProperty(key); + final String valueFromSource = allProperties.get(key); + + Assert.assertNotNull(valueFromSource); + Assert.assertEquals(valueFromFlat, valueFromSource); + } + + // Verify that no property appears in multiple sources + final Map propertyCount = new HashMap<>(); + propertiesBySource.forEach((source, props) -> { + props.keySet().forEach(key -> propertyCount.put(key, propertyCount.getOrDefault(key, 0) + 1)); + }); + + propertyCount.forEach((key, count) -> { + Assert.assertEquals(count.intValue(), 1); + }); + + Assert.assertEquals(mergedProperties.size(), allProperties.size()); + } + + @Test + public void testConflictResolutionPriority() throws Exception { + // RuntimeConfiguration + System.setProperty("org.killbill.test", "lowValue"); + + final DefaultKillbillConfigSource testSource = new DefaultKillbillConfigSource((String) null) { + @Override + protected Map getEnvironmentVariables() { + final Map mockEnv = new HashMap<>(); + mockEnv.put(ENVIRONMENT_VARIABLE_PREFIX + "org_killbill_test", "highValue"); + return mockEnv; + } + }; + + testSource.setProperty("org.killbill.test", "lowValue"); + + final Properties properties = testSource.getProperties(); + + final String effectiveValue = properties.getProperty("org.killbill.test"); + Assert.assertEquals(effectiveValue, "highValue"); + } + @Test(groups = "fast") public void testGetProperties() throws URISyntaxException, IOException { final Map configuration = new HashMap<>(); @@ -70,21 +169,66 @@ public void testGetProperties() throws URISyntaxException, IOException { @Test(groups = "fast") public void testGetPropertiesBySource() throws URISyntaxException, IOException { - final Map configuration = new HashMap<>(); - configuration.put("org.killbill.dao.user", "root"); - configuration.put("org.killbill.dao.password", "password"); - - final OSGIConfigProperties configSource = new DefaultKillbillConfigSource(null, configuration); + // RuntimeConfiguration + System.setProperty("org.killbill.dao.user", "root"); + System.setProperty("org.killbill.dao.password", "password"); + + // KillBillDefaults + final Map killbillDefaultConfig = new HashMap<>(); + killbillDefaultConfig.put("org.killbill.server.shutdownDelay", "3s"); + killbillDefaultConfig.put("org.killbill.billing.osgi.dao.logLevel", "INFO"); + + // ImmutableSystemProperties + killbillDefaultConfig.put("user.timezone", "GMT"); + + // EnvironmentVariables + final OSGIConfigProperties configSource = new DefaultKillbillConfigSource(null, killbillDefaultConfig) { + @Override + protected Map getEnvironmentVariables() { + final Map mockEnv = new HashMap<>(); + mockEnv.put(ENVIRONMENT_VARIABLE_PREFIX + "org_killbill_dao_healthCheckConnectionTimeout", "11s"); + mockEnv.put(ENVIRONMENT_VARIABLE_PREFIX + "org_killbill_billing_osgi_dao_maxActive", "99"); + + return mockEnv; + } + }; final Map> propsBySource = configSource.getPropertiesBySource(); Assert.assertNotNull(propsBySource); Assert.assertFalse(propsBySource.isEmpty()); - final Map defaultProps = propsBySource.get("ExtraDefaultProperties"); - Assert.assertNotNull(defaultProps); - Assert.assertEquals(defaultProps.get("org.killbill.dao.user"), "root"); - Assert.assertEquals(defaultProps.get("org.killbill.dao.password"), "password"); + Assert.assertTrue(propsBySource.containsKey("ImmutableSystemProperties")); + + final Map immutableProps = propsBySource.get("ImmutableSystemProperties"); + Assert.assertEquals(immutableProps.get("user.timezone"), "GMT"); + + Assert.assertTrue(propsBySource.containsKey("EnvironmentVariables")); + + final Map environmentVariables = propsBySource.get("EnvironmentVariables"); + Assert.assertEquals(environmentVariables.get("org.killbill.dao.healthCheckConnectionTimeout"), "11s"); + Assert.assertEquals(environmentVariables.get("org.killbill.billing.osgi.dao.maxActive"), "99"); + + Assert.assertTrue(propsBySource.containsKey("RuntimeConfiguration")); + + final Map runtimeConfig = propsBySource.get("RuntimeConfiguration"); + Assert.assertEquals(runtimeConfig.get("org.killbill.dao.user"), "root"); + Assert.assertEquals(runtimeConfig.get("org.killbill.dao.password"), "password"); + + Assert.assertTrue(propsBySource.containsKey("KillBillDefaults")); + + final Map killBillDefaults = propsBySource.get("KillBillDefaults"); + Assert.assertEquals(killBillDefaults.get("org.killbill.server.shutdownDelay"), "3s"); + Assert.assertEquals(killBillDefaults.get("org.killbill.billing.osgi.dao.logLevel"), "INFO"); + + final List actualSourceOrder = new ArrayList<>(propsBySource.keySet()); + + final List expectedPrecedenceOrder = Arrays.asList("ImmutableSystemProperties", + "EnvironmentVariables", + "RuntimeConfiguration", + "KillBillDefaults"); + + Assert.assertEquals(actualSourceOrder, expectedPrecedenceOrder); } @Test(groups = "fast") diff --git a/platform-test/src/main/java/org/killbill/billing/platform/test/config/TestKillbillConfigSource.java b/platform-test/src/main/java/org/killbill/billing/platform/test/config/TestKillbillConfigSource.java index 4709b94c1..68860e5fd 100644 --- a/platform-test/src/main/java/org/killbill/billing/platform/test/config/TestKillbillConfigSource.java +++ b/platform-test/src/main/java/org/killbill/billing/platform/test/config/TestKillbillConfigSource.java @@ -54,7 +54,7 @@ public TestKillbillConfigSource(@Nullable final String file, @Nullable final Cla // Set default System Properties before creating the instance of DBTestingHelper. Whereas MySQL loads its // driver at startup, h2 loads it statically and we need System Properties set at that point - populateDefaultProperties(); + populateDefaultProperties(extraDefaults); if (dbTestingHelperKlass != null) { final PlatformDBTestingHelper dbTestingHelper = (PlatformDBTestingHelper) dbTestingHelperKlass.getDeclaredMethod("get").invoke(null); @@ -71,7 +71,8 @@ public TestKillbillConfigSource(@Nullable final String file, @Nullable final Cla this.extraDefaults = extraDefaults; // extraDefaults changed, need to reload defaults - populateDefaultProperties(); + populateDefaultProperties(extraDefaults); + rebuildCache(); } @Override