diff --git a/.gitignore b/.gitignore index 8906c71da1..93b1d9e73b 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ server/test/io.cloudbeaver.test.platform/workspace/.data/ .classpath .settings/ +## Eclipse PDE +*.product.launch + workspace-dev-ce/ deploy/cloudbeaver server/**/target diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java index 516ef29e2a..e9d3887df0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseServerConfigurationController.java @@ -19,12 +19,32 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; +import org.jkiss.utils.IOUtils; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; /** * Abstract class that contains methods for loading configuration with gson. */ public abstract class BaseServerConfigurationController implements WebServerConfigurationController { + private static final Log log = Log.getLog(BaseServerConfigurationController.class); + @NotNull + private final Path homeDirectory; + + protected Path workspacePath; + + protected BaseServerConfigurationController(@NotNull Path homeDirectory) { + this.homeDirectory = homeDirectory; + //default workspaceLocation + this.workspacePath = homeDirectory.resolve("workspace"); + } @NotNull public Gson getGson() { @@ -34,4 +54,50 @@ public Gson getGson() { protected abstract GsonBuilder getGsonBuilder(); public abstract T getServerConfiguration(); + + + @NotNull + protected synchronized void initWorkspacePath() throws DBException { + if (workspacePath != null && !IOUtils.isFileFromDefaultFS(workspacePath)) { + log.warn("Workspace directory already initialized: " + workspacePath); + } + String workspaceLocation = getWorkspaceLocation(); + URI workspaceUri = URI.create(workspaceLocation); + if (workspaceUri.getScheme() == null) { + // default filesystem + this.workspacePath = getHomeDirectory().resolve(workspaceLocation); + } else { + var externalFsProvider = + FileSystemProviderRegistry.getInstance().getFileSystemProviderBySchema(workspaceUri.getScheme()); + if (externalFsProvider == null) { + throw new DBException("File system not found for scheme: " + workspaceUri.getScheme()); + } + ClassLoader fsClassloader = externalFsProvider.getInstance().getClass().getClassLoader(); + try (FileSystem externalFileSystem = FileSystems.newFileSystem(workspaceUri, + System.getenv(), + fsClassloader);) { + this.workspacePath = externalFileSystem.provider().getPath(workspaceUri); + } catch (Exception e) { + throw new DBException("Failed to initialize workspace path: " + workspaceUri, e); + } + } + log.info("Workspace path initialized: " + workspacePath); + } + + @NotNull + protected abstract String getWorkspaceLocation(); + + @NotNull + protected Path getHomeDirectory() { + return homeDirectory; + } + + @NotNull + @Override + public Path getWorkspacePath() { + if (workspacePath == null) { + throw new RuntimeException("Workspace path not initialized"); + } + return workspacePath; + } } 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 9483767290..810c387d47 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 @@ -233,6 +233,12 @@ public String getWorkspaceIdProperty() throws DBException { return BaseWorkspaceImpl.readWorkspaceIdProperty(); } + @Override + public Path getWorkspaceDirectory() { + return getServerConfigurationController().getWorkspacePath(); + } + + public String getApplicationId() { try { return getApplicationInstanceId(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java index c06f137b70..f6823f6dfb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfigurationController.java @@ -39,6 +39,11 @@ default Map getOriginalConfigurationProperties() { return Map.of(); } + @NotNull + Path getWorkspacePath(); + @NotNull Gson getGson(); + + void validateFinalServerConfiguration() throws DBException; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 0e42353e31..b2b33f11a9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -433,7 +433,7 @@ public String moveResource( throw new DBException("Resource '" + oldTargetPath + "' doesn't exists"); } Path newTargetPath = getTargetPath(projectId, normalizedNewResourcePath); - validateResourcePath(newTargetPath.toString()); + validateResourcePath(rootPath.relativize(newTargetPath).toString()); if (Files.exists(newTargetPath)) { throw new DBException("Resource with name %s already exists".formatted(newTargetPath.getFileName())); } @@ -672,7 +672,7 @@ private Path getTargetPath(@NotNull String projectId, @NotNull String resourcePa if (!targetPath.startsWith(projectPath)) { throw new DBException("Invalid resource path"); } - return WebAppUtils.getWebApplication().getHomeDirectory().relativize(targetPath); + return targetPath; } catch (InvalidPathException e) { throw new DBException("Resource path contains invalid characters"); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java index bf966ea748..b87526cd92 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java @@ -23,6 +23,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPWorkspace; +import org.jkiss.utils.IOUtils; import java.io.IOException; import java.io.Reader; @@ -73,18 +74,23 @@ public RMFileLockController(WebApplication application, int maxLockTime) throws * @return - lock */ @NotNull - public RMLock lockProject(@NotNull String projectId,@NotNull String operationName) throws DBException { + public RMLock lockProject(@NotNull String projectId, @NotNull String operationName) throws DBException { synchronized (RMFileLockController.class) { try { - createLockFolderIfNeeded(); - createProjectFolder(projectId); - Path projectLockFile = getProjectLockFilePath(projectId); - RMLockInfo lockInfo = new RMLockInfo.Builder(projectId, UUID.randomUUID().toString()) .setApplicationId(applicationId) .setOperationName(operationName) .setOperationStartTime(System.currentTimeMillis()) .build(); + Path projectLockFile = getProjectLockFilePath(projectId); + + if (!IOUtils.isFileFromDefaultFS(lockFolderPath)) { + // fake lock for external file system? + return new RMLock(projectLockFile); + } + createLockFolderIfNeeded(); + createProjectFolder(projectId); + createLockFile(projectLockFile, lockInfo); return new RMLock(projectLockFile); } catch (Exception e) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java index ca5d26fd6a..a2ae8040a2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/BaseGQLPlatform.java @@ -17,6 +17,7 @@ package io.cloudbeaver.server; import io.cloudbeaver.DBWConstants; +import io.cloudbeaver.model.app.WebApplication; import org.eclipse.core.runtime.Plugin; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; @@ -33,6 +34,8 @@ import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl; import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; import java.io.IOException; import java.nio.file.Files; @@ -40,7 +43,7 @@ public abstract class BaseGQLPlatform extends BasePlatformImpl { private static final Log log = Log.getLog(BaseGQLPlatform.class); - public static final String WORK_DATA_FOLDER_NAME = ".work-data"; + public static final String BASE_TEMP_DIR = "dbeaver"; private Path tempFolder; @@ -55,7 +58,7 @@ protected synchronized void initialize() { SecurityProviderUtils.registerSecurityProvider(); // Register properties adapter - this.workspace = new WebGlobalWorkspace(this); + this.workspace = new WebGlobalWorkspace(this, (WebApplication) getApplication()); this.workspace.initializeProjects(); QMUtils.initApplication(this); @@ -92,16 +95,12 @@ public DBPWorkspace getWorkspace() { @NotNull public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String name) { + if (tempFolder == null) { - // Make temp folder - monitor.subTask("Create temp folder"); - tempFolder = workspace.getAbsolutePath().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); - } - if (!Files.exists(tempFolder)) { - try { - Files.createDirectories(tempFolder); - } catch (IOException e) { - log.error("Can't create temp directory " + tempFolder, e); + synchronized (this) { + if (tempFolder == null) { + initTempFolder(monitor); + } } } Path folder = tempFolder.resolve(name); @@ -115,6 +114,21 @@ public Path getTempFolder(@NotNull DBRProgressMonitor monitor, @NotNull String n return folder; } + private void initTempFolder(@NotNull DBRProgressMonitor monitor) { + // Make temp folder + monitor.subTask("Create temp folder"); + String sysTempFolder = System.getProperty(StandardConstants.ENV_TMP_DIR); + if (CommonUtils.isNotEmpty(sysTempFolder)) { + tempFolder = Path.of(sysTempFolder).resolve(BASE_TEMP_DIR).resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } else { + //we do not use workspace because it can be in external file system + tempFolder = getApplication().getHomeDirectory().resolve(DBWConstants.WORK_DATA_FOLDER_NAME); + } + } + + @NotNull + public abstract WebApplication getApplication(); + @Override public synchronized void dispose() { super.dispose(); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java index 997c0dcded..a06f682699 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebGlobalWorkspace.java @@ -19,7 +19,6 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.model.app.WebApplication; import io.cloudbeaver.utils.WebAppUtils; -import org.eclipse.core.runtime.Platform; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; @@ -30,8 +29,6 @@ import org.jkiss.utils.CommonUtils; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -49,21 +46,14 @@ public class WebGlobalWorkspace extends BaseWorkspaceImpl { protected final Map projects = new LinkedHashMap<>(); private WebGlobalProject globalProject; - public WebGlobalWorkspace(DBPPlatform platform) { - super(platform, - platform.getApplication().isMultiuser() - ? Path.of(getWorkspaceURI()) - : ((WebApplication) platform.getApplication()).getWorkspaceDirectory()); - } + private final WebApplication application; - @NotNull - private static URI getWorkspaceURI() { - String workspacePath = Platform.getInstanceLocation().getURL().toString(); - try { - return new URI(workspacePath); - } catch (URISyntaxException e) { - throw new IllegalStateException("Workspace path is invalid: " + workspacePath, e); - } + public WebGlobalWorkspace( + @NotNull DBPPlatform platform, + @NotNull WebApplication application + ) { + super(platform, application.getWorkspaceDirectory()); + this.application = application; } @Override diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java index 94652dd478..d13eb03bcf 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebPlatformActivator.java @@ -23,7 +23,6 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import java.io.File; import java.io.PrintStream; /** @@ -33,7 +32,6 @@ public class WebPlatformActivator extends Plugin { // The shared instance private static WebPlatformActivator instance; - private static File configDir; private PrintStream debugWriter; private DBPPreferenceStore preferences; 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 0bf84aae33..18fde20361 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 @@ -59,13 +59,13 @@ import org.jkiss.utils.StandardConstants; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URL; import java.net.UnknownHostException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -109,6 +109,8 @@ public static CBApplication getInstance() { private final Map initActions = new ConcurrentHashMap<>(); + private CBJettyServer jettyServer; + public CBApplication() { this.homeDirectory = new File(initHomeFolder()); } @@ -201,6 +203,7 @@ protected void startServer() { if (!loadServerConfiguration()) { return; } + if (CommonUtils.isEmpty(this.getAppConfiguration().getDefaultUserTeam())) { throw new DBException("Default user team must be specified"); } @@ -208,6 +211,7 @@ protected void startServer() { log.error(e); return; } + refreshDisabledDriversConfig(); configurationMode = CommonUtils.isEmpty(getServerConfiguration().getServerName()); @@ -303,7 +307,7 @@ protected void startServer() { if (configurationMode) { // Try to configure automatically - performAutoConfiguration(getMainConfigurationFilePath().toFile().getParentFile()); + performAutoConfiguration(getMainConfigurationFilePath().getParent()); } else if (!isMultiNode()) { var appConfiguration = getServerConfigurationController().getAppConfiguration(); if (appConfiguration.isGrantConnectionsAccessToAnonymousTeam()) { @@ -331,7 +335,7 @@ protected void initializeAdditionalConfiguration() { * * @param configPath */ - protected void performAutoConfiguration(File configPath) { + protected void performAutoConfiguration(Path configPath) { String autoServerName = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_NAME); String autoServerURL = System.getenv(CBConstants.VAR_AUTO_CB_SERVER_URL); String autoAdminName = System.getenv(CBConstants.VAR_AUTO_CB_ADMIN_NAME); @@ -340,11 +344,11 @@ protected void performAutoConfiguration(File configPath) { if (CommonUtils.isEmpty(autoServerName) || CommonUtils.isEmpty(autoAdminName) || CommonUtils.isEmpty( autoAdminPassword)) { // Try to load from auto config file - if (configPath.exists()) { - File autoConfigFile = new File(configPath, CBConstants.AUTO_CONFIG_FILE_NAME); - if (autoConfigFile.exists()) { + if (Files.exists(configPath)) { + Path autoConfigFile = configPath.resolve(CBConstants.AUTO_CONFIG_FILE_NAME); + if (Files.exists(autoConfigFile)) { Properties autoProps = new Properties(); - try (InputStream is = new FileInputStream(autoConfigFile)) { + try (InputStream is = Files.newInputStream(autoConfigFile)) { autoProps.load(is); autoServerName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_SERVER_NAME); @@ -352,7 +356,7 @@ protected void performAutoConfiguration(File configPath) { autoAdminName = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_NAME); autoAdminPassword = autoProps.getProperty(CBConstants.VAR_AUTO_CB_ADMIN_PASSWORD); } catch (IOException e) { - log.error("Error loading auto configuration file '" + autoConfigFile.getAbsolutePath() + "'", + log.error("Error loading auto configuration file '" + autoConfigFile + "'", e); } } @@ -439,11 +443,6 @@ public Path getDataDirectory(boolean create) { return dataDir.toPath(); } - @Override - public Path getWorkspaceDirectory() { - return Path.of(getServerConfiguration().getWorkspaceLocation()); - } - private void initializeSecurityController() throws DBException { securityController = createGlobalSecurityController(); } @@ -468,7 +467,8 @@ private void runWebServer() { getServerPort(), CommonUtils.isEmpty(getServerHost()) ? "all interfaces" : getServerHost()) ); - new CBJettyServer(this).runServer(); + this.jettyServer = new CBJettyServer(this); + this.jettyServer.runServer(); } @@ -568,6 +568,9 @@ public synchronized void reloadConfiguration(@Nullable SMCredentialsProvider cre sendConfigChangedEvent(credentialsProvider); eventController.setForceSkipEvents(isConfigurationMode()); + if (this.jettyServer != null) { + this.jettyServer.refreshJettyConfig(); + } } protected abstract void finishSecurityServiceConfiguration( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java index 7acde3722b..883e326314 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -42,6 +42,7 @@ import org.jkiss.dbeaver.utils.PrefUtils; import org.jkiss.dbeaver.utils.SystemVariablesResolver; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; import java.io.*; import java.net.InetAddress; @@ -68,6 +69,7 @@ public abstract class CBServerConfigurationController private final Map originalConfigurationProperties = new LinkedHashMap<>(); protected CBServerConfigurationController(@NotNull T serverConfiguration, @NotNull Path homeDirectory) { + super(homeDirectory); this.serverConfiguration = serverConfiguration; this.homeDirectory = homeDirectory; } @@ -91,17 +93,25 @@ public void loadServerConfiguration(Path configPath) throws DBException { loadConfiguration(configPath); } + initWorkspacePath(); + // Try to load configuration from runtime app config file Path runtimeConfigPath = getRuntimeAppConfigPath(); if (Files.exists(runtimeConfigPath)) { log.debug("Runtime configuration [" + runtimeConfigPath.toAbsolutePath() + "]"); loadConfiguration(runtimeConfigPath); } - // Set default preferences PrefUtils.setDefaultPreferenceValue(DBWorkbench.getPlatform().getPreferenceStore(), ModelPreferences.UI_DRIVERS_HOME, getServerConfiguration().getDriversLocation()); + validateFinalServerConfiguration(); + } + + @NotNull + @Override + protected String getWorkspaceLocation() { + return getServerConfiguration().getWorkspaceLocation(); } public void loadConfiguration(Path configPath) throws DBException { @@ -146,7 +156,7 @@ protected void parseConfiguration(Map configProps) throws DBExce ); // App config Map appConfig = JSONUtils.getObject(configProps, "app"); - validateConfiguration(appConfig); + preValidateAppConfiguration(appConfig); gson.fromJson(gson.toJson(appConfig), CBAppConfig.class); readProductConfiguration(serverConfig, gson); } @@ -169,7 +179,6 @@ public T parseServerConfiguration() { config.setContentRoot(WebAppUtils.getRelativePath(config.getContentRoot(), homeDirectory)); config.setRootURI(readRootUri(config.getRootURI())); config.setDriversLocation(WebAppUtils.getRelativePath(config.getDriversLocation(), homeDirectory)); - config.setWorkspaceLocation(WebAppUtils.getRelativePath(config.getWorkspaceLocation(), homeDirectory)); String staticContentsFile = config.getStaticContent(); if (!CommonUtils.isEmpty(staticContentsFile)) { @@ -182,10 +191,11 @@ public T parseServerConfiguration() { return config; } - protected void validateConfiguration(Map appConfig) throws DBException { + protected void preValidateAppConfiguration(Map appConfig) throws DBException { } + private void readExternalProperties(Map serverConfig) { String externalPropertiesFile = JSONUtils.getString(serverConfig, CBConstants.PARAM_EXTERNAL_PROPERTIES); if (!CommonUtils.isEmpty(externalPropertiesFile)) { @@ -248,19 +258,21 @@ protected void readProductConfiguration(Map serverConfig, Gson g } } - // 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)) { - var runtimeProductSettings = JSONUtils.parseMap(gson, reader); - var productSettings = serverConfiguration.getProductSettings(); - runtimeProductSettings.putAll(productSettings); - Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); - productSettings.clear(); - productSettings.putAll(flattenConfig); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); + if (workspacePath != null && IOUtils.isFileFromDefaultFS(getWorkspacePath())) { + // Add product config from runtime + Path rtConfig = getRuntimeProductConfigFilePath(); + if (Files.exists(rtConfig)) { + log.debug("Load product runtime configuration from '" + rtConfig + "'"); + try (Reader reader = new InputStreamReader(Files.newInputStream(rtConfig), StandardCharsets.UTF_8)) { + var runtimeProductSettings = JSONUtils.parseMap(gson, reader); + var productSettings = serverConfiguration.getProductSettings(); + runtimeProductSettings.putAll(productSettings); + Map flattenConfig = WebAppUtils.flattenMap(runtimeProductSettings); + productSettings.clear(); + productSettings.putAll(flattenConfig); + } catch (Exception e) { + throw new DBException("Error reading product runtime configuration", e); + } } } } @@ -307,7 +319,7 @@ protected Map readConfiguration(Path configPath) throws DBExcept } public Map readConfigurationFile(Path path) throws DBException { - try (Reader reader = new InputStreamReader(new FileInputStream(path.toFile()), StandardCharsets.UTF_8)) { + try (Reader reader = new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8)) { return JSONUtils.parseMap(getGson(), reader); } catch (Exception e) { throw new DBException("Error parsing server configuration", e); @@ -358,8 +370,7 @@ private synchronized void writeRuntimeConfig(Path runtimeConfigPath, Map productConfiguration) throws DBException { @@ -633,4 +646,9 @@ private String readRootUri(String uri) { public Map getOriginalConfigurationProperties() { return originalConfigurationProperties; } + + @Override + public void validateFinalServerConfiguration() throws DBException { + + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java index 7031b941a6..d51c1f7cbd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java @@ -100,6 +100,4 @@ protected GsonBuilder getGsonBuilder() { return gsonBuilder .registerTypeAdapter(WebDatabaseConfig.class, dbConfigCreator); } - - } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 7cee228b18..4c7cfde0cc 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -61,6 +61,7 @@ public class CBJettyServer { } private final CBApplication application; + private Server server; public CBJettyServer(@NotNull CBApplication application) { this.application = application; @@ -69,7 +70,6 @@ public CBJettyServer(@NotNull CBApplication application) { public void runServer() { try { CBServerConfig serverConfiguration = application.getServerConfiguration(); - Server server; int serverPort = serverConfiguration.getServerPort(); String serverHost = serverConfiguration.getServerHost(); Path sslPath = getSslConfigurationPath(); @@ -198,7 +198,7 @@ public void runServer() { } } } - + refreshJettyConfig(); server.start(); server.join(); } catch (Exception e) { @@ -224,6 +224,7 @@ public static void initSessionManager( ) { // Init sessions persistence CBSessionHandler sessionHandler = new CBSessionHandler(application); + sessionHandler.setRefreshCookieAge(CBSessionHandler.ONE_MINUTE); int intMaxIdleSeconds; if (maxIdleTime > Integer.MAX_VALUE) { log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead"); @@ -232,6 +233,7 @@ public static void initSessionManager( intMaxIdleSeconds = (int) (maxIdleTime / 1000); log.debug("Max http session idle time: " + intMaxIdleSeconds + "s"); sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds); + sessionHandler.setMaxCookieAge(intMaxIdleSeconds); DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); sessionCache.setSessionDataStore(new NullSessionDataStore()); @@ -241,6 +243,19 @@ public static void initSessionManager( DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); idMgr.setWorkerName(null); server.addBean(idMgr, true); + } + public synchronized void refreshJettyConfig() { + if (server == null) { + return; + } + log.info("Refreshing Jetty configuration"); + if (server.getHandler() instanceof ServletContextHandler servletContextHandler + && servletContextHandler.getSessionHandler() instanceof CBSessionHandler cbSessionHandler + ) { + cbSessionHandler.setMaxCookieAge((int) (application.getMaxSessionIdleTime() / 1000)); + var serverUrl = this.application.getServerURL(); + cbSessionHandler.setSecureCookies(serverUrl != null && serverUrl.startsWith("https://")); + } } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java index 6fa6d0f7b8..b8539264f7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBSessionHandler.java @@ -17,174 +17,13 @@ package io.cloudbeaver.server.jetty; import io.cloudbeaver.server.GQLApplicationAdapter; -import jakarta.servlet.SessionCookieConfig; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.SessionHandler; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; - public class CBSessionHandler extends SessionHandler { - private final CBCookieConfig cbCookieConfig; + static final int ONE_MINUTE = 60; private final GQLApplicationAdapter application; public CBSessionHandler(GQLApplicationAdapter application) { - this.cbCookieConfig = new CBCookieConfig(); this.application = application; } - - - @Override - public SessionCookieConfig getSessionCookieConfig() { - return this.cbCookieConfig; - } - - - //mostly copy of org.eclipse.jetty.ee10.servlet.CookieConfig but allows to use dynamic setSecure flag - public final class CBCookieConfig implements SessionCookieConfig { - - @Override - public boolean isSecure() { - var serverUrl = CBSessionHandler.this.application.getServerURL(); - return serverUrl != null && serverUrl.startsWith("https://"); - } - - @Override - public String getComment() { - return getSessionComment(); - } - - @Override - public String getDomain() { - return getSessionDomain(); - } - - @Override - public int getMaxAge() { - return getMaxCookieAge(); - } - - @Override - public void setAttribute(String name, String value) { - checkState(); - String lcase = name.toLowerCase(Locale.ENGLISH); - - switch (lcase) { - case "name" -> setName(value); - case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value)); - case "comment" -> setComment(value); - case "domain" -> setDomain(value); - case "httponly" -> setHttpOnly(Boolean.parseBoolean(value)); - case "secure" -> setSecure(Boolean.parseBoolean(value)); - case "path" -> setPath(value); - default -> setSessionCookieAttribute(name, value); - } - } - - @Override - public String getAttribute(String name) { - String lcase = name.toLowerCase(Locale.ENGLISH); - return switch (lcase) { - case "name" -> getName(); - case "max-age" -> Integer.toString(getMaxAge()); - case "comment" -> getComment(); - case "domain" -> getDomain(); - case "httponly" -> String.valueOf(isHttpOnly()); - case "secure" -> String.valueOf(isSecure()); - case "path" -> getPath(); - default -> getSessionCookieAttribute(name); - }; - } - - /** - * According to the SessionCookieConfig javadoc, the attributes must also include - * all values set by explicit setters. - * - * @see SessionCookieConfig - */ - @Override - public Map getAttributes() { - Map specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - specials.put("name", getAttribute("name")); - specials.put("max-age", getAttribute("max-age")); - specials.put("comment", getAttribute("comment")); - specials.put("domain", getAttribute("domain")); - specials.put("httponly", getAttribute("httponly")); - specials.put("secure", getAttribute("secure")); - specials.put("path", getAttribute("path")); - specials.putAll(getSessionCookieAttributes()); - return Collections.unmodifiableMap(specials); - } - - @Override - public String getName() { - return getSessionCookie(); - } - - @Override - public String getPath() { - return getSessionPath(); - } - - @Override - public boolean isHttpOnly() { - return CBSessionHandler.this.isHttpOnly(); - } - - @Override - public void setComment(String comment) { - checkState(); - CBSessionHandler.this.setSessionComment(comment); - } - - @Override - public void setDomain(String domain) { - checkState(); - CBSessionHandler.this.setSessionDomain(domain); - } - - @Override - public void setHttpOnly(boolean httpOnly) { - checkState(); - CBSessionHandler.this.setHttpOnly(httpOnly); - } - - @Override - public void setMaxAge(int maxAge) { - checkState(); - CBSessionHandler.this.setMaxCookieAge(maxAge); - } - - @Override - public void setName(String name) { - checkState(); - CBSessionHandler.this.setSessionCookie(name); - } - - @Override - public void setPath(String path) { - checkState(); - CBSessionHandler.this.setSessionPath(path); - } - - @Override - public void setSecure(boolean secure) { - checkState(); - CBSessionHandler.this.setSecureCookies(secure); - } - - private void checkState() { - //It is allowable to call the CookieConfig.setXX methods after the SessionHandler has started, - //but before the context has fully started. Ie it is allowable for ServletContextListeners - //to call these methods in contextInitialized(). - ServletContextHandler handler = ServletContextHandler.getCurrentServletContextHandler(); - if (handler != null && handler.isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - - } - } - - } diff --git a/webapp/packages/core-blocks/src/Clickable.tsx b/webapp/packages/core-blocks/src/Clickable.tsx index 81f3aba9a0..d18656fd70 100644 --- a/webapp/packages/core-blocks/src/Clickable.tsx +++ b/webapp/packages/core-blocks/src/Clickable.tsx @@ -6,13 +6,13 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { type ClickableOptions, Clickable as MantineClickable } from 'reakit'; +import { type ClickableOptions, Clickable as ReakitClickable } from 'reakit'; import type { ReakitProxyComponent, ReakitProxyComponentOptions } from './Menu/ReakitProxyComponent.js'; export const Clickable: ReakitProxyComponent<'button', ClickableOptions> = observer>( function Clickable({ children, ...rest }) { - const Component = MantineClickable; + const Component = ReakitClickable; return {children}; }, diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index c3550a12a0..4891f550f2 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -45,6 +45,7 @@ export default [ ['ui_error_message', 'Error:'], ['ui_error_close', 'Close'], ['ui_clear', 'Clear'], + ['ui_clean', 'Clean'], ['ui_remove', 'Remove'], ['ui_close', 'Close'], ['ui_open', 'Open'], diff --git a/webapp/packages/core-localization/src/locales/fr.ts b/webapp/packages/core-localization/src/locales/fr.ts index ff2b1a88cf..a3318e7ec5 100644 --- a/webapp/packages/core-localization/src/locales/fr.ts +++ b/webapp/packages/core-localization/src/locales/fr.ts @@ -43,6 +43,7 @@ export default [ ['ui_error_message', 'Erreur :'], ['ui_error_close', 'Fermer'], ['ui_clear', 'Effacer'], + ['ui_clean', 'Clean'], ['ui_remove', 'Supprimer'], ['ui_close', 'Fermer'], ['ui_open', 'Ouvrir'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index a878a93543..9249b97f15 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -42,6 +42,7 @@ export default [ ['ui_error_message', 'Errore:'], ['ui_error_close', 'Chiudi'], ['ui_clear', 'Clear'], + ['ui_clean', 'Clean'], ['ui_remove', 'Remove'], ['ui_close', 'Chiudi'], ['ui_open', 'Open'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index ade1239035..34a1e60cf8 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -41,6 +41,7 @@ export default [ ['ui_error_message', 'Ошибка:'], ['ui_error_close', 'Закрыть'], ['ui_clear', 'Очистить'], + ['ui_clean', 'Очистить'], ['ui_remove', 'Убрать'], ['ui_close', 'Закрыть'], ['ui_open', 'Открыть'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index f951ec042f..e2e5eb7b07 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -42,6 +42,7 @@ export default [ ['ui_error_message', '错误:'], ['ui_error_close', '关闭'], ['ui_clear', '清除'], + ['ui_clean', 'Clean'], ['ui_remove', '移除'], ['ui_close', '关闭'], ['ui_open', '打开'], diff --git a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts index ce916549a8..dc92787812 100644 --- a/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts +++ b/webapp/packages/plugin-data-viewer-result-set-grouping/src/Actions/ACTION_DATA_VIEWER_GROUPING_CLEAR.ts @@ -8,7 +8,7 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_DATA_VIEWER_GROUPING_CLEAR = createAction('data-viewer-grouping-clear', { - label: 'ui_clear', - tooltip: 'ui_clear', + label: 'ui_clean', + tooltip: 'ui_clean', icon: 'erase', });