From f6ec0111075aa8406889bfeb49c4cc10f38d8eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= <91755733+lds217@users.noreply.github.com> Date: Sat, 2 Aug 2025 05:12:27 +0700 Subject: [PATCH] [update] Refactor Server class to streamline database connection handling and configuration loading. Replace HikariCP with direct SQLite connection for improved simplicity. Enhance logging for server address and port configuration. Update Google Drive integration methods and add functionality to save configuration changes. Clean up unused methods and ensure proper initialization of server components. --- src/main/java/BsK/server/Server.java | 287 +++++++++++------- .../java/BsK/server/network/entity/User.java | 2 +- .../server/network/handler/ServerHandler.java | 6 +- 3 files changed, 186 insertions(+), 109 deletions(-) diff --git a/src/main/java/BsK/server/Server.java b/src/main/java/BsK/server/Server.java index aab5115..14671d9 100644 --- a/src/main/java/BsK/server/Server.java +++ b/src/main/java/BsK/server/Server.java @@ -2,8 +2,6 @@ package BsK.server; import BsK.server.network.handler.ServerHandler; import BsK.server.service.GoogleDriveServiceOAuth; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; @@ -25,32 +23,32 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.SQLException; +import java.sql.Statement; import java.util.Properties; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @Slf4j public class Server { - private static int PORT; - - // --- REFACTORED DATABASE CONNECTION --- - // The single, static connection pool (DataSource). This is thread-safe. - private static HikariDataSource dataSource; - - // These static fields are still okay as they are configured once at startup. - public static GoogleDriveServiceOAuth googleDriveService; - private static boolean googleDriveEnabled = true; - private static String googleDriveRootFolderName = "BSK_Clinic_Patient_Files"; - public static String imageDbPath = "img_db"; - public static String checkupMediaBaseDir = "src/main/resources/image/checkup_media"; + private static int PORT; + public static Connection connection; + public static GoogleDriveServiceOAuth googleDriveService; + private static boolean googleDriveEnabled = true; // Can be configured + private static String googleDriveRootFolderName = "BSK_Clinic_Patient_Files"; // Default + public static String imageDbPath = "img_db"; // Default + public static String checkupMediaBaseDir = "src/main/resources/image/checkup_media"; // Default - // Static initializer block to set up the server configuration and connection pool static { try { - // --- Load configuration --- + // Load configuration Properties props = new Properties(); + + // First try to load from external config file File externalConfig = new File("config/config.properties"); if (externalConfig.exists()) { try (var input = Files.newInputStream(externalConfig.toPath())) { @@ -58,6 +56,7 @@ public class Server { log.info("Loaded configuration from external file: {}", externalConfig.getAbsolutePath()); } } else { + // Fall back to internal config try (InputStream input = Server.class.getClassLoader().getResourceAsStream("config.properties")) { if (input == null) { log.error("Unable to find config.properties"); @@ -67,108 +66,67 @@ public class Server { log.info("Loaded configuration from internal resources"); } } + + // Get port configuration + String portStr = props.getProperty("server.port"); + String addressStr = props.getProperty("server.address"); + + // Log address configuration + if (addressStr != null) { + log.info("Server address configured as: {}", addressStr); + } else { + log.info("No server address configured, clients will need to use the correct address to connect"); + } + + // Get port configuration + if (portStr != null) { + PORT = Integer.parseInt(portStr); + log.info("Using configured port: {}", PORT); + } else { + PORT = 1999; + log.info("Using default port: {}", PORT); + } - PORT = Integer.parseInt(props.getProperty("server.port", "1999")); - log.info("Using port: {}", PORT); - - // Load other properties - googleDriveEnabled = Boolean.parseBoolean(props.getProperty("google.drive.enabled", "true")); - googleDriveRootFolderName = props.getProperty("google.drive.root.folder", "BSK_Clinic_Patient_Files").trim(); - imageDbPath = props.getProperty("storage.image_db_path", "img_db").trim(); - checkupMediaBaseDir = props.getProperty("storage.checkup_media_base_dir", "src/main/resources/image/checkup_media").trim(); - log.info("Google Drive enabled: {}", googleDriveEnabled); + // Get Google Drive configuration + String driveEnabledStr = props.getProperty("google.drive.enabled"); + if (driveEnabledStr != null) { + googleDriveEnabled = Boolean.parseBoolean(driveEnabledStr); + } + + String driveRootFolderStr = props.getProperty("google.drive.root.folder"); + if (driveRootFolderStr != null && !driveRootFolderStr.trim().isEmpty()) { + googleDriveRootFolderName = driveRootFolderStr.trim(); + } + + log.info("Google Drive integration enabled: {}", googleDriveEnabled); log.info("Google Drive root folder: {}", googleDriveRootFolderName); + + // Get Storage paths configuration + String imageDbPathStr = props.getProperty("storage.image_db_path"); + if (imageDbPathStr != null && !imageDbPathStr.trim().isEmpty()) { + imageDbPath = imageDbPathStr.trim(); + } + + String checkupMediaBaseDirStr = props.getProperty("storage.checkup_media_base_dir"); + if (checkupMediaBaseDirStr != null && !checkupMediaBaseDirStr.trim().isEmpty()) { + checkupMediaBaseDir = checkupMediaBaseDirStr.trim(); + } + log.info("Image DB path: {}", imageDbPath); log.info("Checkup media base dir: {}", checkupMediaBaseDir); - - // --- DATABASE POOL INITIALIZATION --- + // Directly use the provided database path String dbPath = "src/main/resources/database/BSK.db"; - - // 1. Configure HikariCP - HikariConfig config = new HikariConfig(); - config.setJdbcUrl("jdbc:sqlite:" + dbPath); - config.setConnectionTestQuery("SELECT 1"); - config.setMaximumPoolSize(10); // Max 10 concurrent connections, perfect for your use case - config.setPoolName("BsK-DB-Pool"); - config.addDataSourceProperty("cachePrepStmts", "true"); - config.addDataSourceProperty("prepStmtCacheSize", "250"); - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - - // 2. Create the DataSource (the pool itself) - dataSource = new HikariDataSource(config); - log.info("✅ Database connection pool initialized successfully for {}", dbPath); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); // Initialize Google Drive service if enabled initializeGoogleDriveService(); } catch (Exception e) { - log.error("❌ Failed to initialize server", e); throw new RuntimeException("Failed to initialize server", e); } } - /** - * This is the new, safe way for handlers to get a database connection. - * @return A database connection from the pool. - * @throws SQLException if a connection cannot be obtained. - */ - public static Connection getConnection() throws SQLException { - return dataSource.getConnection(); - } - - public static void main(String[] args) throws InterruptedException { - log.info("\n____ _____ _ __\n| _ \\ / ____| |/ \n| |_) | (___ | ' / \n| _ < \\___ \\| <\n| |_) |____) | . \\\n|____/|_____/|_|\\_\n"); - ServerDashboard dashboard = ServerDashboard.getInstance(); - dashboard.setVisible(true); - dashboard.updateStatus("Starting...", Color.ORANGE); - dashboard.updatePort(PORT); - dashboard.updateGoogleDriveStatus(isGoogleDriveConnected(), isGoogleDriveConnected() ? "Connected" : "Disconnected"); - - EventLoopGroup parentGroup = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); - EventLoopGroup childGroup = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); - try { - ServerBootstrap bootstrap = new ServerBootstrap() - .group(parentGroup, childGroup) - .channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) - .localAddress(new InetSocketAddress(PORT)) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline() - .addLast(new IdleStateHandler(60 * 30, 0, 0, TimeUnit.SECONDS)) - .addLast(new HttpServerCodec()) - .addLast(new ChunkedWriteHandler()) - .addLast(new HttpObjectAggregator(50 * 1024 * 1024)) - .addLast(new WebSocketServerProtocolHandler("/", null, true, 50 * 1024 * 1024)) - .addLast(new ServerHandler()); // A new handler for each client - } - }); - - ChannelFuture f = bootstrap.bind().sync(); - log.info("Server started on port {}", PORT); - dashboard.updateStatus("Running", Color.GREEN); - dashboard.addLog("Server started successfully on port " + PORT); - - f.channel().closeFuture().sync(); - } catch (Exception e) { - dashboard.updateStatus("Error", Color.RED); - dashboard.addLog("Server error: " + e.getMessage()); - log.error("Server crashed", e); - } finally { - dashboard.updateStatus("Shutting down", Color.ORANGE); - dashboard.addLog("Server shutting down..."); - parentGroup.shutdownGracefully(); - childGroup.shutdownGracefully(); - if (dataSource != null) { - dataSource.close(); // Close the connection pool on shutdown - log.info("Database connection pool closed."); - } - } - } - - // --- Google Drive and other helper methods --- - private static void initializeGoogleDriveService() { if (!googleDriveEnabled) { log.info("Google Drive integration is disabled"); @@ -181,6 +139,7 @@ public class Server { googleDriveService = new GoogleDriveServiceOAuth(googleDriveRootFolderName); log.info("✅ Google Drive OAuth service initialized successfully"); + // Update dashboard if it exists if (ServerDashboard.getInstance() != null) { ServerDashboard.getInstance().updateGoogleDriveStatus(true, "Connected"); } @@ -188,31 +147,48 @@ public class Server { } catch (Exception e) { log.error("❌ Failed to initialize Google Drive OAuth service: {}", e.getMessage()); log.info("💡 Server will continue without Google Drive integration"); + log.info("🔧 To fix this, ensure google-oauth-credentials.json is properly configured"); googleDriveService = null; + // Update dashboard if it exists if (ServerDashboard.getInstance() != null) { ServerDashboard.getInstance().updateGoogleDriveStatus(false, "Failed: " + e.getMessage()); } } } + /** + * Manually retry Google Drive connection + */ public static void retryGoogleDriveConnection() { log.info("🔄 Retrying Google Drive connection..."); initializeGoogleDriveService(); } + /** + * Check if Google Drive is connected + */ public static boolean isGoogleDriveConnected() { return googleDriveService != null; } + /** + * Get Google Drive service instance + */ public static GoogleDriveServiceOAuth getGoogleDriveService() { return googleDriveService; } + /** + * Get the current Google Drive root folder name + */ public static String getGoogleDriveRootFolderName() { return googleDriveRootFolderName; } + /** + * Update Google Drive root folder name and reinitialize service + */ public static void updateGoogleDriveRootFolder(String newRootFolderName) throws Exception { if (newRootFolderName == null || newRootFolderName.trim().isEmpty()) { throw new IllegalArgumentException("Root folder name cannot be empty"); @@ -221,17 +197,23 @@ public class Server { googleDriveRootFolderName = newRootFolderName.trim(); log.info("🔄 Updating Google Drive root folder to: {}", googleDriveRootFolderName); + // Save to configuration file saveGoogleDriveRootFolderToConfig(googleDriveRootFolderName); + // Reinitialize Google Drive service with new root folder if (googleDriveEnabled) { initializeGoogleDriveService(); } } + /** + * Save Google Drive root folder name to config file + */ private static void saveGoogleDriveRootFolderToConfig(String rootFolderName) { try { Properties props = new Properties(); + // Load existing properties File externalConfig = new File("config/config.properties"); if (externalConfig.exists()) { try (var input = Files.newInputStream(externalConfig.toPath())) { @@ -245,13 +227,16 @@ public class Server { } } + // Update the root folder property props.setProperty("google.drive.root.folder", rootFolderName); + // Create config directory if it doesn't exist File configDir = new File("config"); if (!configDir.exists()) { configDir.mkdirs(); } + // Save to external config file try (var output = Files.newOutputStream(externalConfig.toPath())) { props.store(output, "BSK Server Configuration - Updated by Dashboard"); log.info("💾 Saved Google Drive root folder setting to config file"); @@ -262,6 +247,96 @@ public class Server { } } + private static String extractDatabaseFile() throws IOException { + throw new UnsupportedOperationException("extractDatabaseFile is not used anymore. Database is read directly from the specified path."); + } + + public static Statement statement; + + static { + try { + statement = connection.createStatement(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public Server() throws SQLException { + } + + public static void main(String[] args) throws InterruptedException, SQLException { + log.info("\n____ _____ _ __\n| _ \\ / ____| |/\n| |_) | (___ | ' /\n| _ < \\___ \\| <\n| |_) |____) | . \\\n|____/|_____/|_|\\_\n"); + // Initialize and show server dashboard + ServerDashboard dashboard = ServerDashboard.getInstance(); + dashboard.setVisible(true); + dashboard.updateStatus("Starting...", Color.ORANGE); + dashboard.updatePort(PORT); + + // Update Google Drive status on dashboard + dashboard.updateGoogleDriveStatus(isGoogleDriveConnected(), + isGoogleDriveConnected() ? "Connected" : "Disconnected"); + + EventLoopGroup parentGroup = + Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); + EventLoopGroup childGroup = + Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); + try { + ServerBootstrap bootstrap = + new ServerBootstrap() + .group(parentGroup, childGroup) + .channel( + Epoll.isAvailable() + ? EpollServerSocketChannel.class + : NioServerSocketChannel.class) + .localAddress(new InetSocketAddress(PORT)) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline() + .addLast(new IdleStateHandler(60 * 30, 0, 0, TimeUnit.SECONDS)) + .addLast(new HttpServerCodec()) + .addLast(new ChunkedWriteHandler()) + .addLast(new HttpObjectAggregator(50 * 1024 * 1024)) + .addLast(new WebSocketServerProtocolHandler("/", null, true, 50 * 1024 * 1024)) + .addLast(new ServerHandler()); + } + }); + + ChannelFuture f = bootstrap.bind().sync(); + + log.info("Server started on port {}", PORT); + log.info("To connect, clients should use:"); + log.info(" - If on same machine: ws://localhost:{} or ws://127.0.0.1:{}", PORT, PORT); + log.info(" - If on network: ws://:{}", PORT); + + // Update dashboard status to running + dashboard.updateStatus("Running", Color.GREEN); + dashboard.addLog("Server started successfully on port " + PORT); + if (isGoogleDriveConnected()) { + dashboard.addLog("Google Drive integration: Connected ✅"); + } else { + dashboard.addLog("Google Drive integration: Disconnected ❌"); + } + + f.channel().closeFuture().sync(); + } catch (Exception e) { + dashboard.updateStatus("Error", Color.RED); + dashboard.addLog("Server error: " + e.getMessage()); + throw e; + } finally { + dashboard.updateStatus("Shutting down", Color.ORANGE); + dashboard.addLog("Server shutting down..."); + parentGroup.shutdownGracefully(); + childGroup.shutdownGracefully(); + } + } + + /** + * Initiates the backup of the main database file to Google Drive. + * This is intended to be called from the ServerDashboard. + * @throws IOException if the file cannot be found or the upload fails. + */ public static void backupDatabaseToDrive() throws IOException { ServerDashboard dashboard = ServerDashboard.getInstance(); @@ -270,6 +345,7 @@ public class Server { throw new IOException("Google Drive service is not available."); } + // Define the path to the database String dbPath = "src/main/resources/database/BSK.db"; java.io.File dbFile = new java.io.File(dbPath); @@ -278,6 +354,7 @@ public class Server { throw new IOException("Database file not found: " + dbPath); } + // Call the service to perform the backup googleDriveService.backupDatabaseFile(dbFile); - } -} + } +} \ No newline at end of file diff --git a/src/main/java/BsK/server/network/entity/User.java b/src/main/java/BsK/server/network/entity/User.java index 7f55646..81e3a3d 100644 --- a/src/main/java/BsK/server/network/entity/User.java +++ b/src/main/java/BsK/server/network/entity/User.java @@ -58,4 +58,4 @@ public class User { SessionManager.updateUserRole(channel.id().asLongText(), this.role.toString(), this.userId); log.info("User session {} has been de-authenticated.", sessionId); } -} +} \ No newline at end of file diff --git a/src/main/java/BsK/server/network/handler/ServerHandler.java b/src/main/java/BsK/server/network/handler/ServerHandler.java index 604466b..aca7547 100644 --- a/src/main/java/BsK/server/network/handler/ServerHandler.java +++ b/src/main/java/BsK/server/network/handler/ServerHandler.java @@ -596,7 +596,7 @@ public class ServerHandler extends SimpleChannelInboundHandler