diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 057034b0e0..d896c86a31 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -28,6 +28,7 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.registry.BaseApplicationImpl; import org.jkiss.dbeaver.registry.EclipseWorkspaceImpl; @@ -120,8 +121,12 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements return LocalResourceController.builder(credentialsProvider).build(); } + protected Map getServerConfigProps(Map configProps) { + return JSONUtils.getObject(configProps, "server"); + } + @SuppressWarnings("unchecked") - protected void patchConfigurationWithProperties(Map configProps, IVariableResolver varResolver) { + public static void patchConfigurationWithProperties(Map configProps, IVariableResolver varResolver) { for (Map.Entry entry : configProps.entrySet()) { Object propValue = entry.getValue(); if (propValue instanceof String) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java index 6e6e3123fd..e079966cdd 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java @@ -20,6 +20,10 @@ import io.cloudbeaver.model.app.WebApplication; import org.jkiss.dbeaver.runtime.DBWorkbench; import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; public class WebAppUtils { public static String getRelativePath(String path, String curDir) { @@ -37,4 +41,32 @@ public class WebAppUtils { return (WebApplication) DBWorkbench.getPlatform().getApplication(); } + @SuppressWarnings("unchecked") + public static Map mergeConfigurations(Map origin, Map additional) { + var resultConfig = new HashMap(); + Set rootKeys = new HashSet<>(origin.keySet()); + rootKeys.addAll(additional.keySet()); + + for (var rootKey : rootKeys) { + var originValue = origin.get(rootKey); + var additionalValue = additional.get(rootKey); + + if (originValue == null || additionalValue == null) { + var resultValue = originValue != null ? originValue : additionalValue; + resultConfig.put(rootKey, resultValue); + continue; + } + + if (originValue instanceof Map) { + var resultValue = mergeConfigurations((Map) originValue, (Map) additionalValue); + resultConfig.put(rootKey, resultValue); + } else { + resultConfig.put(rootKey, additionalValue); + } + + } + + return resultConfig; + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index e84c417658..d084be891d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -19,7 +19,6 @@ package io.cloudbeaver.server; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; -import com.google.gson.stream.JsonWriter; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provider.AuthProviderConfig; import io.cloudbeaver.model.app.BaseWebApplication; @@ -106,7 +105,7 @@ public class CBApplication extends BaseWebApplication { private File homeDirectory; // Configurations - private final Map productConfiguration = new HashMap<>(); + protected final Map productConfiguration = new HashMap<>(); private final Map databaseConfiguration = new HashMap<>(); private final CBAppConfig appConfiguration = new CBAppConfig(); private Map externalProperties = new LinkedHashMap<>(); @@ -478,6 +477,11 @@ public class CBApplication extends BaseWebApplication { } private void parseConfiguration(File configFile) throws DBException { + Map configProps = readConfiguration(configFile); + parseConfiguration(configProps); + } + + private void parseConfiguration(Map configProps) throws DBException { String homeFolder = System.getenv(CBConstants.ENV_CB_HOME); if (CommonUtils.isEmpty(homeFolder)) { homeFolder = System.getProperty("user.dir"); @@ -486,40 +490,11 @@ public class CBApplication extends BaseWebApplication { homeFolder = "."; } homeDirectory = new File(homeFolder); - String productConfigPath = null; CBAppConfig prevConfig = new CBAppConfig(appConfiguration); - - // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) - InstanceCreator appConfigCreator = type -> appConfiguration; - InstanceCreator navSettingsCreator = type -> (DataSourceNavigatorSettings) appConfiguration.getDefaultNavigatorSettings(); - - Gson gson = new GsonBuilder() - .setLenient() - .registerTypeAdapter(CBAppConfig.class, appConfigCreator) - .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) - .create(); - - try (Reader reader = new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8)) { - Map configProps = JSONUtils.parseMap(gson, reader); - + Gson gson = getGson(); + try { Map serverConfig = JSONUtils.getObject(configProps, "server"); - - String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); - if (!CommonUtils.isEmpty(externalPropertiesFile)) { - Properties props = new Properties(); - try (InputStream is = Files.newInputStream(Path.of(externalPropertiesFile))) { - props.load(is); - } catch (IOException e) { - log.error("Error loading external properties from " + externalPropertiesFile, e); - } - for (String propName : props.stringPropertyNames()) { - this.externalProperties.put(propName, props.getProperty(propName)); - } - } - - patchConfigurationWithProperties(configProps); - serverPort = JSONUtils.getInteger(serverConfig, CBConstants.PARAM_SERVER_PORT, serverPort); serverHost = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_HOST, serverHost); if (serverConfig.containsKey(CBConstants.PARAM_SERVER_URL)) { @@ -552,14 +527,7 @@ public class CBApplication extends BaseWebApplication { gson.fromJson(gson.toJsonTree(appConfig), CBAppConfig.class); databaseConfiguration.putAll(JSONUtils.getObject(serverConfig, CBConstants.PARAM_DB_CONFIGURATION)); - productConfigPath = WebAppUtils.getRelativePath( - JSONUtils.getString( - serverConfig, - CBConstants.PARAM_PRODUCT_CONFIGURATION, - CBConstants.DEFAULT_PRODUCT_CONFIGURATION - ), - homeFolder - ); + readProductConfiguration(serverConfig, gson, homeFolder); String staticContentsFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_STATIC_CONTENT); if (!CommonUtils.isEmpty(staticContentsFile)) { @@ -591,6 +559,19 @@ public class CBApplication extends BaseWebApplication { appConfiguration.setAuthProvidersConfiguration(mergedAuthProviders); } + patchConfigurationWithProperties(productConfiguration); + } + + protected void readProductConfiguration(Map serverConfig, Gson gson, String homeFolder) throws DBException { + String productConfigPath = WebAppUtils.getRelativePath( + JSONUtils.getString( + serverConfig, + CBConstants.PARAM_PRODUCT_CONFIGURATION, + CBConstants.DEFAULT_PRODUCT_CONFIGURATION + ), + homeFolder + ); + if (!CommonUtils.isEmpty(productConfigPath)) { File productConfigFile = new File(productConfigPath); if (!productConfigFile.exists()) { @@ -606,18 +587,58 @@ public class CBApplication extends BaseWebApplication { } // Add product config from runtime - { - File rtConfig = getRuntimeProductConfigFilePath().toFile(); - if (rtConfig.exists()) { - log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); - } + File rtConfig = getRuntimeProductConfigFilePath().toFile(); + if (rtConfig.exists()) { + log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); + try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { + productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); + } catch (Exception e) { + throw new DBException("Error reading product runtime configuration", e); } } - patchConfigurationWithProperties(productConfiguration); + } + + protected Map readConfiguration(File configFile) throws DBException { + try (Reader reader = new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8)) { + Map configProps = JSONUtils.parseMap(getGson(), reader); + patchConfigurationWithProperties(configProps); // patch original properties + readAdditionalConfiguration(configProps); + Map serverConfig = getServerConfigProps(configProps); + + String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); + if (!CommonUtils.isEmpty(externalPropertiesFile)) { + Properties props = new Properties(); + try (InputStream is = Files.newInputStream(Path.of(externalPropertiesFile))) { + props.load(is); + } catch (IOException e) { + log.error("Error loading external properties from " + externalPropertiesFile, e); + } + for (String propName : props.stringPropertyNames()) { + this.externalProperties.put(propName, props.getProperty(propName)); + } + } + + patchConfigurationWithProperties(configProps); // patch again because properties can be changed + return configProps; + } catch (IOException e) { + throw new DBException("Error parsing server configuration", e); + } + } + + private Gson getGson() { + // Stupid way to populate existing objects but ok google (https://github.com/google/gson/issues/431) + InstanceCreator appConfigCreator = type -> appConfiguration; + InstanceCreator navSettingsCreator = type -> (DataSourceNavigatorSettings) appConfiguration.getDefaultNavigatorSettings(); + + return new GsonBuilder() + .setLenient() + .registerTypeAdapter(CBAppConfig.class, appConfigCreator) + .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) + .create(); + } + + protected void readAdditionalConfiguration(Map rootConfig) throws DBException { + } protected void parseAdditionalServerConfiguration(Map serverConfig) throws DBException { @@ -702,10 +723,9 @@ public class CBApplication extends BaseWebApplication { // Re-load runtime configuration try { log.debug("Reloading application configuration"); - File runtimeConfigFile = getRuntimeAppConfigFile(); - if (runtimeConfigFile.exists()) { - log.debug("Runtime configuration [" + runtimeConfigFile.getAbsolutePath() + "]"); - parseConfiguration(runtimeConfigFile); + Map runtimeConfigProps = readRuntimeConfigurationProperties(); + if (!runtimeConfigProps.isEmpty()) { + parseConfiguration(runtimeConfigProps); } } catch (Exception e) { throw new DBException("Error parsing configuration", e); @@ -714,6 +734,15 @@ public class CBApplication extends BaseWebApplication { configurationMode = CommonUtils.isEmpty(serverName); } + private Map readRuntimeConfigurationProperties() throws DBException { + File runtimeConfigFile = getRuntimeAppConfigFile(); + if (runtimeConfigFile.exists()) { + log.debug("Runtime configuration [" + runtimeConfigFile.getAbsolutePath() + "]"); + return readConfiguration(runtimeConfigFile); + } + return Map.of(); + } + protected void finishSecurityServiceConfiguration(@NotNull String adminName, @Nullable String adminPassword, @NotNull List authInfoList) throws DBException { SecurityPluginService.finishConfiguration(adminName, adminPassword, authInfoList); } @@ -741,106 +770,116 @@ public class CBApplication extends BaseWebApplication { } } - private void saveRuntimeConfig(String newServerName, String newServerURL, long sessionExpireTime, CBAppConfig appConfig) throws DBException { + protected void saveRuntimeConfig(String newServerName, String newServerURL, long sessionExpireTime, CBAppConfig appConfig) throws DBException { + Map configurationProperties = collectConfigurationProperties(newServerName, newServerURL, sessionExpireTime, appConfig); + writeRuntimeConfig(configurationProperties); + } + private void writeRuntimeConfig(Map configurationProperties) throws DBException { File runtimeConfigFile = getRuntimeAppConfigFile(); + try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigFile), StandardCharsets.UTF_8)) { Gson gson = new GsonBuilder() .setLenient() .setPrettyPrinting() .create(); - try (JsonWriter json = gson.newJsonWriter(out)) { - json.setLenient(true); - json.beginObject(); - { - json.name("server"); - json.beginObject(); - if (!CommonUtils.isEmpty(newServerName)) { - JSONUtils.field(json, CBConstants.PARAM_SERVER_NAME, newServerName); - } - if (!CommonUtils.isEmpty(newServerURL)) { - JSONUtils.field(json, CBConstants.PARAM_SERVER_URL, newServerURL); - } - if (sessionExpireTime > 0) { - JSONUtils.field(json, CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime); - } - - if (!CommonUtils.isEmpty(databaseConfiguration)) { - JSONUtils.serializeProperties(json, CBConstants.PARAM_DB_CONFIGURATION, databaseConfiguration); - } - json.endObject(); - } - { - json.name("app"); - json.beginObject(); - JSONUtils.field(json, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); - JSONUtils.field(json, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); - JSONUtils.field(json, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); - JSONUtils.field(json, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); - JSONUtils.field(json, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); - JSONUtils.field(json, "forwardProxy", appConfig.isEnabledForwardProxy()); - JSONUtils.field(json, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled()); - - Map resourceQuotas = appConfig.getResourceQuotas(); - if (!CommonUtils.isEmpty(resourceQuotas)) { - JSONUtils.serializeProperties(json, CBConstants.PARAM_RESOURCE_QUOTAS, resourceQuotas); - } - - { - // Save only differences in def navigator settings - DBNBrowseSettings navSettings = appConfig.getDefaultNavigatorSettings(); - - json.name("defaultNavigatorSettings"); - json.beginObject(); - if (navSettings.isShowSystemObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowSystemObjects()) - JSONUtils.field(json, "showSystemObjects", navSettings.isShowSystemObjects()); - if (navSettings.isShowUtilityObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowUtilityObjects()) - JSONUtils.field(json, "showUtilityObjects", navSettings.isShowUtilityObjects()); - if (navSettings.isShowOnlyEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowOnlyEntities()) - JSONUtils.field(json, "showOnlyEntities", navSettings.isShowOnlyEntities()); - if (navSettings.isMergeEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isMergeEntities()) - JSONUtils.field(json, "mergeEntities", navSettings.isMergeEntities()); - if (navSettings.isHideFolders() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideFolders()) - JSONUtils.field(json, "hideFolders", navSettings.isHideFolders()); - if (navSettings.isHideSchemas() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideSchemas()) - JSONUtils.field(json, "hideSchemas", navSettings.isHideSchemas()); - if (navSettings.isHideVirtualModel() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideVirtualModel()) - JSONUtils.field(json, "hideVirtualModel", navSettings.isHideVirtualModel()); - - json.endObject(); - } - if (appConfig.getEnabledFeatures() != null) { - JSONUtils.serializeStringList(json, "enabledFeatures", Arrays.asList(appConfig.getEnabledFeatures()), true); - } - if (appConfig.getEnabledAuthProviders() != null) { - JSONUtils.serializeStringList(json, "enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders()), true); - } - if (appConfig.getEnabledDrivers() != null) { - JSONUtils.serializeStringList(json, "enabledDrivers", Arrays.asList(appConfig.getEnabledDrivers()), true); - } - if (appConfig.getDisabledDrivers() != null) { - JSONUtils.serializeStringList(json, "disabledDrivers", Arrays.asList(appConfig.getDisabledDrivers()), true); - } - - if (!CommonUtils.isEmpty(appConfig.getPlugins())) { - JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins()); - } - if (!CommonUtils.isEmpty(appConfig.getAuthProviderConfigurations())) { - json.name("authConfiguration"); - gson.toJson(appConfig.getAuthProviderConfigurations(), Map.class, json); - } - - - json.endObject(); - } - json.endObject(); - } + gson.toJson(configurationProperties, out); } catch (IOException e) { throw new DBException("Error writing runtime configuration", e); } } + protected Map collectConfigurationProperties( + String newServerName, + String newServerURL, + long sessionExpireTime, + CBAppConfig appConfig + ) { + Map rootConfig = new LinkedHashMap<>(); + { + var serverConfigProperties = new LinkedHashMap(); + rootConfig.put("server", serverConfigProperties); + if (!CommonUtils.isEmpty(newServerName)) { + serverConfigProperties.put(CBConstants.PARAM_SERVER_NAME, newServerName); + } + if (!CommonUtils.isEmpty(newServerURL)) { + serverConfigProperties.put(CBConstants.PARAM_SERVER_URL, newServerURL); + } + if (sessionExpireTime > 0) { + serverConfigProperties.put(CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime); + } + + if (!CommonUtils.isEmpty(databaseConfiguration)) { + serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfiguration); + } + } + { + var appConfigProperties = new LinkedHashMap(); + rootConfig.put("app", appConfigProperties); + + appConfigProperties.put("anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); + appConfigProperties.put("supportsCustomConnections", appConfig.isSupportsCustomConnections()); + appConfigProperties.put("publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); + appConfigProperties.put("adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); + appConfigProperties.put("enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); + appConfigProperties.put("forwardProxy", appConfig.isEnabledForwardProxy()); + appConfigProperties.put(CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled()); + + Map resourceQuotas = appConfig.getResourceQuotas(); + appConfigProperties.put(CBConstants.PARAM_RESOURCE_QUOTAS, resourceQuotas); + + { + // Save only differences in def navigator settings + DBNBrowseSettings navSettings = appConfig.getDefaultNavigatorSettings(); + var navigatorProperties = new LinkedHashMap(); + appConfigProperties.put("defaultNavigatorSettings", navigatorProperties); + + if (navSettings.isShowSystemObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowSystemObjects()) { + navigatorProperties.put("showSystemObjects", navSettings.isShowSystemObjects()); + } + if (navSettings.isShowUtilityObjects() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowUtilityObjects()) { + navigatorProperties.put("showUtilityObjects", navSettings.isShowUtilityObjects()); + } + if (navSettings.isShowOnlyEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isShowOnlyEntities()) { + navigatorProperties.put("showOnlyEntities", navSettings.isShowOnlyEntities()); + } + if (navSettings.isMergeEntities() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isMergeEntities()) { + navigatorProperties.put("mergeEntities", navSettings.isMergeEntities()); + } + if (navSettings.isHideFolders() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideFolders()) { + navigatorProperties.put("hideFolders", navSettings.isHideFolders()); + } + if (navSettings.isHideSchemas() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideSchemas()) { + navigatorProperties.put("hideSchemas", navSettings.isHideSchemas()); + } + if (navSettings.isHideVirtualModel() != CBAppConfig.DEFAULT_VIEW_SETTINGS.isHideVirtualModel()) { + navigatorProperties.put("hideVirtualModel", navSettings.isHideVirtualModel()); + } + } + if (appConfig.getEnabledFeatures() != null) { + appConfigProperties.put("enabledFeatures", Arrays.asList(appConfig.getEnabledFeatures())); + } + if (appConfig.getEnabledAuthProviders() != null) { + appConfigProperties.put("enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders())); + } + if (appConfig.getEnabledDrivers() != null) { + appConfigProperties.put("enabledDrivers", Arrays.asList(appConfig.getEnabledDrivers())); + } + if (appConfig.getDisabledDrivers() != null) { + appConfigProperties.put("disabledDrivers", Arrays.asList(appConfig.getDisabledDrivers())); + } + + if (!CommonUtils.isEmpty(appConfig.getPlugins())) { + appConfigProperties.put("plugins", appConfig.getPlugins()); + } + if (!CommonUtils.isEmpty(appConfig.getAuthProviderConfigurations())) { + appConfigProperties.put("authConfiguration", appConfig.getAuthProviderConfigurations()); + } + } + return rootConfig; + } + //////////////////////////////////////////////////////////////////////// // License management diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java index 440efce0b2..d6b4eecc01 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/ConfigurationUtils.java @@ -38,4 +38,5 @@ public class ConfigurationUtils { return true; } + }