diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 660cce095..2939242ed 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,4 +7,4 @@ on:
jobs:
ci:
- uses: killbill/gh-actions-shared/.github/workflows/ci.yml@main
+ uses: vnandwana/gh-actions-shared/.github/workflows/ci.yml@test
diff --git a/.idea/misc.xml b/.idea/misc.xml
index f4a499a6b..0eeb962ff 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -18,6 +18,7 @@
+
\ No newline at end of file
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..930869284 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,64 +100,174 @@ 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(extraDefaultProperties);
- populateDefaultProperties();
+ //rebuildCache();
if (Boolean.parseBoolean(getString(LOOKUP_ENVIRONMENT_VARIABLES))) {
overrideWithEnvironmentVariables();
+ // rebuildCache();
}
if (Boolean.parseBoolean(getString(ENABLE_JASYPT_DECRYPTION))) {
decryptJasyptProperties();
+ //rebuildCache();
}
}
@Override
public String getString(final String propertyName) {
- return properties.getProperty(propertyName);
+ if (cachedPropertiesBySource == null) {
+ return getPropertyDirect(propertyName);
+ }
+
+ 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 +277,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 +288,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 +332,10 @@ protected void populateDefaultProperties() {
//
System.setProperty(propertyName, GMT_ID);
TimeZone.setDefault(TimeZone.getTimeZone(GMT_ID));
+
+ immutableProps.put(PROP_USER_TIME_ZONE, GMT_ID);
+ // defaultsToAdd.put(propertyName, GMT_ID);
+
continue;
}
@@ -219,6 +343,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 +361,33 @@ protected void populateDefaultProperties() {
}
}
- defaultSystemProperties.putAll(defaultProperties);
-
- final Map propsMap = propertiesToMap(defaultSystemProperties);
- propertiesCollector.addProperties("DefaultSystemProperties", propsMap);
- }
-
- @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);
- }
- });
+ if (!immutableProps.isEmpty()) {
+ propertiesCollector.addProperties("ImmutableSystemProperties", immutableProps);
+ }
- final Map> propertiesBySource = propertiesCollector.getPropertiesBySource();
+ // defaultSystemProperties.putAll(defaultProperties);
- final Map> result = new LinkedHashMap<>();
+ // final Map propsMap = propertiesToMap(defaultSystemProperties);
+ // propertiesCollector.addProperties("KillBillDefaults", propsMap);
- propertiesBySource.forEach((source, properties) -> {
- final Map sourceProperties = new LinkedHashMap<>();
- properties.forEach(prop -> {
- sourceProperties.put(prop.getKey(), prop.getValue());
- });
- result.put(source, Collections.unmodifiableMap(sourceProperties));
- });
+ if (!defaultsToAdd.isEmpty()) {
+ propertiesCollector.addProperties("KillBillDefaults", defaultsToAdd);
+ }
+ }
- 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 +397,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 +418,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 +430,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 +449,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