diff --git a/build.gradle b/build.gradle index 97e7f44..b22e124 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,11 @@ dependencies { implementation 'me.friwi:jcefmaven:122.1.10' implementation 'com.alphacephei:vosk:0.3.45' implementation 'net.java.dev.jna:jna:5.13.0' + implementation 'com.h2database:h2:2.2.220' + implementation 'org.xerial:sqlite-jdbc:3.41.2.1' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.postgresql:postgresql:42.6.0' + implementation 'net.java.dev.jna:jna-platform:5.13.0' implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'com.google.guava:guava:31.1-jre' diff --git a/javascript/DatabaseTool.html b/javascript/DatabaseTool.html new file mode 100644 index 0000000..8720d00 --- /dev/null +++ b/javascript/DatabaseTool.html @@ -0,0 +1,793 @@ + + + + + + 数据库管理工具 - Axis Innovators Box + + + +
+ + +
+ + +
+
+ +
+ + + + +
+
+ + + + +
+
状态: 就绪
+
+ +
+
+
+
查询编辑器
+
+ + +
+
+ +
+ +
+
+
查询结果
+
+
+
+
+

请连接数据库并执行查询以查看结果

+
+
+
+
+ +
+
就绪
+
行: 0, 列: 0
+
+
+
+ + + + + +
+ + + + + + + + + + + + + diff --git a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java index 6f95af6..17476a4 100644 --- a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java +++ b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java @@ -6,33 +6,121 @@ import org.apache.logging.log4j.Logger; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; /** - * 将输出传递给 Log4j2 的日志记录器 + * 将输出传递给 Log4j2 的日志记录器,同时保持控制台输出 + * 修复问题:控制台输出被Log4j2覆盖 * @author tzdwindows 7 */ public class Log4j2OutputStream extends OutputStream { private static final Logger logger = LogManager.getLogger(); + + // 恢复静态变量 public static final ByteArrayOutputStream systemOutContent = new ByteArrayOutputStream(); public static final ByteArrayOutputStream systemErrContent = new ByteArrayOutputStream(); + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final boolean isErrorStream; + private final PrintStream originalStream; // 保存原始的控制台流 + + public Log4j2OutputStream(boolean isErrorStream, PrintStream originalStream) { + this.isErrorStream = isErrorStream; + this.originalStream = originalStream; + } + @Override public void write(int b) { - systemOutContent.write(b); - logger.info(String.valueOf((char) b)); + // 写入原始控制台 + originalStream.write(b); + + buffer.write(b); + // 将内容同时写入对应的静态变量 + if (isErrorStream) { + systemErrContent.write(b); + } else { + systemOutContent.write(b); + } + + // 遇到换行符时刷新缓冲区到日志 + if (b == '\n') { + flush(); + } } @Override public void write(byte[] b, int off, int len) { - systemOutContent.write(b, off, len); - String message = new String(b, off, len).trim(); - logger.info(message); + // 写入原始控制台 + originalStream.write(b, off, len); + + buffer.write(b, off, len); + // 将内容同时写入对应的静态变量 + if (isErrorStream) { + systemErrContent.write(b, off, len); + } else { + systemOutContent.write(b, off, len); + } + + // 检查是否包含换行符 + for (int i = off; i < off + len; i++) { + if (b[i] == '\n') { + flush(); + break; + } + } + } + + @Override + public void flush() { + originalStream.flush(); + + String message = buffer.toString(StandardCharsets.UTF_8).trim(); + if (!message.isEmpty()) { + if (isErrorStream) { + logger.error(message); + } else { + logger.info(message); + } + } + buffer.reset(); // 清空缓冲区 + } + + @Override + public void close() { + flush(); + originalStream.close(); } /** - * 重定向 System.out 和 System.err 到 Log4j2 + * 重定向 System.out 和 System.err 到 Log4j2,同时保持控制台输出 */ public static void redirectSystemStreams() { - System.setOut(new PrintStream(new Log4j2OutputStream(), true)); - System.setErr(new PrintStream(new Log4j2OutputStream(), true)); + // 保存原始流 + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + + // System.out 使用 INFO 级别,同时输出到原始控制台 + System.setOut(new PrintStream(new Log4j2OutputStream(false, originalOut), true, StandardCharsets.UTF_8)); + // System.err 使用 ERROR 级别,同时输出到原始控制台 + System.setErr(new PrintStream(new Log4j2OutputStream(true, originalErr), true, StandardCharsets.UTF_8)); + } + + /** + * 清空静态缓冲区内容 + */ + public static void clearBuffers() { + systemOutContent.reset(); + systemErrContent.reset(); + } + + /** + * 获取输出内容 + */ + public static String getSystemOutContent() { + return systemOutContent.toString(StandardCharsets.UTF_8); + } + + public static String getSystemErrContent() { + return systemErrContent.toString(StandardCharsets.UTF_8); } } \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java index 6ecd79e..5272059 100644 --- a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java @@ -11,9 +11,11 @@ import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; import org.cef.callback.CefContextMenuParams; +import org.cef.callback.CefJSDialogCallback; import org.cef.callback.CefMenuModel; import org.cef.callback.CefQueryCallback; import org.cef.handler.*; +import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import javax.swing.*; @@ -425,6 +427,24 @@ public class BrowserWindow extends JFrame { } }); + client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { + @Override + public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { + if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog( + BrowserWindow.this, + message_text, + "警告", + JOptionPane.INFORMATION_MESSAGE + ); + }); + callback.Continue(true, ""); + return true; + } + return false; + } + }); // 3. 拦截所有新窗口(关键修复点!) diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java index b68c87c..ee132d8 100644 --- a/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java @@ -11,9 +11,11 @@ import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; import org.cef.callback.CefContextMenuParams; +import org.cef.callback.CefJSDialogCallback; import org.cef.callback.CefMenuModel; import org.cef.callback.CefQueryCallback; import org.cef.handler.*; +import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import org.json.JSONObject; @@ -432,6 +434,26 @@ public class BrowserWindowJDialog extends JDialog { } }); + // 添加 alert 弹窗监控处理 + client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { + @Override + public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { + if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog( + BrowserWindowJDialog.this, + message_text, + "警告", + JOptionPane.INFORMATION_MESSAGE + ); + }); + callback.Continue(true, ""); + return true; + } + return false; + } + }); + // 3. 拦截所有新窗口(关键修复点!) diff --git a/src/main/java/com/axis/innovators/box/browser/MainApplication.java b/src/main/java/com/axis/innovators/box/browser/MainApplication.java index 95ec6aa..3198055 100644 --- a/src/main/java/com/axis/innovators/box/browser/MainApplication.java +++ b/src/main/java/com/axis/innovators/box/browser/MainApplication.java @@ -1,6 +1,7 @@ package com.axis.innovators.box.browser; import com.axis.innovators.box.browser.util.CodeExecutor; +import com.axis.innovators.box.browser.util.DatabaseConnectionManager; import com.axis.innovators.box.tools.FolderCreator; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -11,6 +12,7 @@ import org.cef.callback.CefQueryCallback; import org.cef.handler.CefMessageRouterHandlerAdapter; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; +import org.json.JSONArray; import org.json.JSONObject; import org.tzd.lm.LM; @@ -20,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.sql.*; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; @@ -351,4 +354,1023 @@ public class MainApplication { .withDefaultOperations() .build(); } + + public static void popupDataBaseWindow() { + // 预加载常用 JDBC 驱动(警告级别,不阻塞 UI) + try { + try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.h2.Driver 未找到"); } + try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.sqlite.JDBC 未找到"); } + try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.postgresql.Driver 未找到"); } + try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: com.mysql.cj.jdbc.Driver 未找到"); } + try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: oracle.jdbc.OracleDriver 未找到"); } + } catch (Throwable t) { + System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage()); + } + + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("Axis Innovators Box 数据库管理工具") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + if (window.get() == null) { + System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null"); + return; + } + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + String type = requestJson.optString("type", ""); + + // 补默认端口与标准化 driver 名称 + String drv = requestJson.optString("driver", "").toLowerCase(); + if (!drv.isEmpty()) { + String port = requestJson.optString("port", "").trim(); + if (port.isEmpty()) { + switch (drv) { + case "mysql": + requestJson.put("port", "3306"); + break; + case "postgresql": + case "postgres": + requestJson.put("port", "5432"); + break; + case "oracle": + requestJson.put("port", "1521"); + break; + } + } + if ("postgres".equals(drv)) requestJson.put("driver", "postgresql"); + } + + // 安全标识符校验 pattern(表名/列名等仅允许常见字符) + final java.util.regex.Pattern SAFE_IDENT = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\.\\$]+$"); + + switch (type) { + // 已有功能:继续使用现有处理函数(假定这些方法在类中定义) + case "connectDatabase": + handleDatabaseConnect(requestJson, callback); + break; + case "createLocalDatabase": + handleCreateLocalDatabase(requestJson, callback); + break; + case "disconnectDatabase": + handleDisconnectDatabase(requestJson, callback); + break; + case "executeQuery": + handleExecuteQuery(requestJson, callback); + break; + case "getTables": + handleGetTables(requestJson, callback); + break; + case "getTableData": + handleGetTableData(requestJson, callback); + break; + case "getTableStructure": + handleGetTableStructure(requestJson, callback); + break; + case "updateTheme": + handleUpdateTheme(requestJson, callback); + break; + case "getFonts": + handleGetFonts(requestJson, callback); + break; + + // 新增后端支持:analyzeQuery(EXPLAIN / EXPLAIN ANALYZE) + case "analyzeQuery": { + String connectionId = requestJson.optString("connectionId", ""); + String query = requestJson.optString("query", "").trim(); + if (connectionId.isEmpty() || query.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 query 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + // 依据驱动选择 EXPLAIN 语法 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + String explainSql = "EXPLAIN " + query; + if ("postgresql".equals(drvName)) { + explainSql = "EXPLAIN ANALYZE " + query; + } + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(explainSql)) { + org.json.JSONArray out = new org.json.JSONArray(); + while (rs.next()) { + // EXPLAIN 输出常为单列文本 + out.put(rs.getString(1)); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("explain", out); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // exportData -> 导出为 CSV 或 JSON,写入用户目录下 .axis_innovators_box/exports/ + case "exportData": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String format = requestJson.optString("format", "csv").toLowerCase(); + if (connectionId.isEmpty() || table.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 table 为空").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + java.nio.file.Path exportDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "exports"); + try { java.nio.file.Files.createDirectories(exportDir); } catch (Exception e) { /* ignore */ } + + String filenameBase = table + "_" + System.currentTimeMillis(); + java.nio.file.Path outPath = exportDir.resolve(filenameBase + (format.equals("json") ? ".json" : ".csv")); + + String query = "SELECT * FROM " + table; + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(query)) { + + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + if ("json".equals(format)) { + org.json.JSONArray arr = new org.json.JSONArray(); + while (rs.next()) { + org.json.JSONObject obj = new org.json.JSONObject(); + for (int i = 1; i <= cols; i++) { + Object val = rs.getObject(i); + obj.put(md.getColumnLabel(i), val == null ? org.json.JSONObject.NULL : val); + } + arr.put(obj); + } + java.nio.file.Files.write(outPath, arr.toString(2).getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + // CSV + try (java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outPath, java.nio.charset.StandardCharsets.UTF_8)) { + // header + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + writer.write("\"" + md.getColumnLabel(i).replace("\"","\"\"") + "\""); + } + writer.write("\n"); + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + Object val = rs.getObject(i); + String cell = val == null ? "" : String.valueOf(val); + writer.write("\"" + cell.replace("\"","\"\"") + "\""); + } + writer.write("\n"); + } + } + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("path", outPath.toAbsolutePath().toString()); + resp.put("message","导出成功"); + callback.success(resp.toString()); + } catch (SQLException | java.io.IOException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导出失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // importCsv -> 从给定 path 导入 CSV,要求首行为列名且列名匹配表字段 + case "importCsv": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String path = requestJson.optString("path", ""); + if (connectionId.isEmpty() || table.isEmpty() || path.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + java.nio.file.Path csvPath = java.nio.file.Paths.get(path); + if (!java.nio.file.Files.exists(csvPath)) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 文件不存在").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + // 读取首行作为列头 + try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(csvPath, java.nio.charset.StandardCharsets.UTF_8)) { + String headerLine = br.readLine(); + if (headerLine == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 为空").toString()); + break; + } + // 简单 CSV 解析(支持双引号),但要求列名没有逗号内部双引号结构复杂情形 + String[] columns = headerLine.split(","); + for (int i = 0; i < columns.length; i++) { + columns[i] = columns[i].trim().replaceAll("^\"|\"$", ""); // 去掉可能的两端引号 + if (!SAFE_IDENT.matcher(columns[i]).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法列名: " + columns[i]).toString()); + return true; + } + } + // 构建 INSERT SQL + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < columns.length; i++) { + if (i > 0) placeholders.append(","); + placeholders.append("?"); + } + String insertSql = "INSERT INTO " + table + " (" + String.join(",", columns) + ") VALUES (" + placeholders.toString() + ")"; + conn.setAutoCommit(false); + int imported = 0; + try (java.sql.PreparedStatement pstmt = conn.prepareStatement(insertSql)) { + String line; + while ((line = br.readLine()) != null) { + // 简单分割(不处理复杂引号内部逗号) + String[] parts = line.split(",", -1); + for (int i = 0; i < columns.length; i++) { + String cell = i < parts.length ? parts[i].trim().replaceAll("^\"|\"$", "") : ""; + pstmt.setString(i + 1, cell.isEmpty() ? null : cell); + } + pstmt.addBatch(); + if (++imported % 500 == 0) pstmt.executeBatch(); + } + pstmt.executeBatch(); + conn.commit(); + } catch (SQLException ex) { + conn.rollback(); + throw ex; + } finally { + conn.setAutoCommit(true); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("imported", imported); + resp.put("message", "导入完成"); + callback.success(resp.toString()); + } catch (Exception ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导入失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // generateEr -> 收集表、列信息并返回 JSON(前端可据此生成 ER 图) + case "generateEr": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + try { + DatabaseMetaData meta = conn.getMetaData(); + org.json.JSONArray tablesArr = new org.json.JSONArray(); + + try (ResultSet rsTables = meta.getTables(conn.getCatalog(), null, null, new String[]{"TABLE"})) { + while (rsTables.next()) { + String tbl = rsTables.getString("TABLE_NAME"); + org.json.JSONObject tObj = new org.json.JSONObject(); + tObj.put("name", tbl); + org.json.JSONArray cols = new org.json.JSONArray(); + try (ResultSet rsCols = meta.getColumns(conn.getCatalog(), null, tbl, null)) { + while (rsCols.next()) { + org.json.JSONObject c = new org.json.JSONObject(); + c.put("name", rsCols.getString("COLUMN_NAME")); + c.put("type", rsCols.getString("TYPE_NAME")); + c.put("size", rsCols.getInt("COLUMN_SIZE")); + c.put("nullable", rsCols.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + cols.put(c); + } + } + tObj.put("columns", cols); + tablesArr.put(tObj); + } + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("er", new JSONObject().put("tables", new JSONArray(tablesArr.toString()))); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","生成 ER 失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // analyzePerformance -> 尝试返回当前数据库会话/进程信息(简单实现,依据数据库类型) + case "analyzePerformance": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + org.json.JSONArray out = new org.json.JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT pid, usename, state, query FROM pg_stat_activity LIMIT 50")) { + while (rs.next()) { + org.json.JSONObject r = new org.json.JSONObject(); + r.put("pid", rs.getObject("pid")); + r.put("user", rs.getString("usename")); + r.put("state", rs.getString("state")); + r.put("query", rs.getString("query")); + out.put(r); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) { + while (rs.next()) { + org.json.JSONObject r = new org.json.JSONObject(); + r.put("Id", rs.getObject("Id")); + r.put("User", rs.getString("User")); + r.put("Host", rs.getString("Host")); + r.put("db", rs.getString("db")); + r.put("Command", rs.getString("Command")); + r.put("Time", rs.getString("Time")); + r.put("State", rs.getString("State")); + r.put("Info", rs.getString("Info")); + out.put(r); + } + } + } else { + // 通用替代:返回当前时间与简单连接信息 + org.json.JSONObject r = new org.json.JSONObject(); + r.put("now", java.time.Instant.now().toString()); + r.put("message","未实现针对该数据库的详细性能查询,返回通用信息"); + out.put(r); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("data", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","性能分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // listUsers -> 列出数据库用户(尝试常用查询) + case "listUsers": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + org.json.JSONArray out = new org.json.JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT usename FROM pg_user")) { + while (rs.next()) { + out.put(rs.getString(1)); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT User, Host FROM mysql.user")) { + while (rs.next()) { + org.json.JSONObject u = new org.json.JSONObject(); + u.put("user", rs.getString("User")); + u.put("host", rs.getString("Host")); + out.put(u); + } + } + } else { + // H2 / SQLite: 列出连接用户或简单返回空 + out.put("not_supported_for_db"); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("users", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","列出用户失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + default: { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "未知的操作类型: " + type); + callback.failure(400, err.toString()); + } + } + return true; + } catch (org.json.JSONException je) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "请求解析失败: " + je.getMessage()); + callback.failure(400, error.toString()); + return true; + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", e.getMessage() == null ? e.toString() : e.getMessage()); + callback.failure(500, error.toString()); + return true; + } + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 查询取消,可选地记录日志 + } + }, true); + } else { + System.err.println("popupDataBaseWindow: msgRouter 为 null,消息路由无法注册"); + } + }); + } + + // 处理数据库连接 + private static void handleDatabaseConnect(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "mysql"); + String host = request.optString("host", "localhost"); + String port = request.optString("port", "3306"); + String database = request.optString("database", ""); + String username = request.optString("username", ""); + String password = request.optString("password", ""); + + // 验证必要参数 + if (database.isEmpty()) { + throw new IllegalArgumentException("数据库名不能为空"); + } + + // 建立真实数据库连接 + String connectionId = DatabaseConnectionManager.connect(driver, host, port, database, username, password); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接成功"); + response.put("connectionId", connectionId); + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + response.put("database", info.database); + response.put("driver", info.driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理创建本地数据库 + private static void handleCreateLocalDatabase(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "sqlite"); + String dbName = request.optString("dbName", "my_database"); + + if (dbName.isEmpty()) { + throw new IllegalArgumentException("数据库名称不能为空"); + } + + // 创建本地数据库 + String connectionId = DatabaseConnectionManager.createLocalDatabase(driver, dbName); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "本地数据库创建成功"); + response.put("connectionId", connectionId); + response.put("database", dbName); + response.put("driver", driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "创建数据库失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理断开数据库连接 + private static void handleDisconnectDatabase(JSONObject request, CefQueryCallback callback) { + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + DatabaseConnectionManager.disconnect(connectionId); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接已断开"); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "断开连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理SQL查询执行 + private static void handleExecuteQuery(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String query = request.optString("query", "").trim(); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (query.isEmpty()) { + throw new IllegalArgumentException("SQL查询不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + long startTime = System.currentTimeMillis(); + + JSONObject response = new JSONObject(); + + // 判断查询类型 + boolean isSelect = query.toLowerCase().startsWith("select"); + boolean isUpdate = query.toLowerCase().startsWith("update") || + query.toLowerCase().startsWith("insert") || + query.toLowerCase().startsWith("delete"); + + if (isSelect) { + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + + } else if (isUpdate) { + statement = connection.createStatement(); + int affectedRows = statement.executeUpdate(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("affectedRows", affectedRows); + response.put("message", "操作成功,影响 " + affectedRows + " 行"); + + } else { + // 其他类型的查询(CREATE, DROP, ALTER等) + statement = connection.createStatement(); + boolean hasResults = statement.execute(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("message", "查询执行成功"); + + if (hasResults) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + } + } + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "查询执行失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + // 关闭资源 + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表列表 + private static void handleGetTables(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet resultSet = null; + + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + JSONArray tables = new JSONArray(); + + // 根据数据库类型获取表信息 + String catalog = null; + String schema = null; + + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + case "sqlite": + case "h2": + // SQLite和H2不需要特定的catalog或schema + break; + } + + DatabaseMetaData metaData = connection.getMetaData(); + resultSet = metaData.getTables(catalog, schema, null, new String[]{"TABLE"}); + + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + String tableType = resultSet.getString("TABLE_TYPE"); + + // 获取表的行数 + int rowCount = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + rowCount = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取行数,忽略错误 + } + + JSONObject table = new JSONObject(); + table.put("name", tableName); + table.put("type", tableType); + table.put("rows", rowCount); + tables.put(table); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tables", tables); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表列表失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表数据 + private static void handleGetTableData(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + int limit = request.optInt("limit", 50); + int offset = request.optInt("offset", 0); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + // 构建分页查询 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String query; + + switch (info.driver.toLowerCase()) { + case "mysql": + query = String.format("SELECT * FROM `%s` LIMIT %d OFFSET %d", tableName, limit, offset); + break; + case "postgresql": + query = String.format("SELECT * FROM \"%s\" LIMIT %d OFFSET %d", tableName, limit, offset); + break; + default: + query = String.format("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, limit, offset); + } + + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + } + + // 获取总行数 + int total = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + total = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取总行数,使用当前数据行数 + total = data.length(); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("columns", columns); + response.put("data", data); + response.put("total", total); + response.put("offset", offset); + response.put("limit", limit); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表数据失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表结构 + private static void handleGetTableStructure(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet resultSet = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + DatabaseMetaData metaData = connection.getMetaData(); + + String catalog = null; + String schema = null; + + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + } + + resultSet = metaData.getColumns(catalog, schema, tableName, null); + + JSONArray columns = new JSONArray(); + while (resultSet.next()) { + JSONObject column = new JSONObject(); + column.put("name", resultSet.getString("COLUMN_NAME")); + column.put("type", resultSet.getString("TYPE_NAME")); + column.put("size", resultSet.getInt("COLUMN_SIZE")); + column.put("nullable", resultSet.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + column.put("defaultValue", resultSet.getString("COLUMN_DEF")); + columns.put(column); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("columns", columns); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表结构失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理主题更新(保持不变) + private static void handleUpdateTheme(JSONObject request, CefQueryCallback callback) { + try { + String theme = request.optString("theme", "light"); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("theme", theme); + response.put("message", "主题已更新为: " + theme); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "主题更新失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理字体获取(保持不变) + private static void handleGetFonts(JSONObject request, CefQueryCallback callback) { + try { + JSONArray fonts = new JSONArray(); + String[] fontList = { + "Segoe UI", "Microsoft YaHei", "SimSun", "Arial", + "Helvetica", "Times New Roman", "Courier New", "Verdana" + }; + + for (String font : fontList) { + fonts.put(font); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("fonts", fonts); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取字体失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } } diff --git a/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java b/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java new file mode 100644 index 0000000..9d87dd0 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java @@ -0,0 +1,397 @@ +package com.axis.innovators.box.browser.util; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * 数据库连接管理器 + * @author tzdwindows 7 + */ +public class DatabaseConnectionManager { + private static final java.util.Map connections = new java.util.concurrent.ConcurrentHashMap<>(); + private static final java.util.Map connectionInfo = new java.util.concurrent.ConcurrentHashMap<>(); + + public static class DatabaseInfo { + public String driver; + public String url; + public String host; + public String port; + public String database; + public String username; + + public DatabaseInfo(String driver, String url, String host, String port, String database, String username) { + this.driver = driver; + this.url = url; + this.host = host; + this.port = port; + this.database = database; + this.username = username; + } + } + + public static String connect(String driver, String host, String port, + String database, String username, String password) throws SQLException { + String connectionId = "conn_" + System.currentTimeMillis(); + + String drv = driver == null ? "" : driver.toLowerCase(); + + // 规范化 database 路径(特别是 Windows 反斜杠问题) + if (database != null) { + database = database.replace("\\", "/"); + } else { + database = ""; + } + + // 先显式加载驱动,避免因为 classloader 问题找不到驱动 + try { + switch (drv) { + case "mysql": + Class.forName("com.mysql.cj.jdbc.Driver"); + break; + case "postgresql": + Class.forName("org.postgresql.Driver"); + break; + case "sqlite": + Class.forName("org.sqlite.JDBC"); + break; + case "oracle": + Class.forName("oracle.jdbc.OracleDriver"); + break; + case "h2": + Class.forName("org.h2.Driver"); + break; + default: + // 不抛出,使后续 URL 构造仍可检查类型 + } + } catch (ClassNotFoundException e) { + throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e); + } + + String url = buildConnectionUrl(driver, host, port, database); + + Connection connection; + Properties props = new Properties(); + if (username != null && !username.isEmpty()) props.setProperty("user", username); + if (password != null && !password.isEmpty()) props.setProperty("password", password); + + switch (drv) { + case "mysql": + props.setProperty("useSSL", "false"); + props.setProperty("serverTimezone", "UTC"); + connection = DriverManager.getConnection(url, props); + break; + case "postgresql": + connection = DriverManager.getConnection(url, props); + break; + case "sqlite": + // sqlite 不需要 props,URL 已经是文件路径(已做过替换) + connection = DriverManager.getConnection(url); + break; + case "oracle": + connection = DriverManager.getConnection(url, props); + break; + case "h2": + // H2 使用默认用户 sa / 空密码(如果需要可调整) + connection = DriverManager.getConnection(url, "sa", ""); + break; + default: + throw new SQLException("不支持的数据库类型: " + driver); + } + + connections.put(connectionId, connection); + connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username)); + return connectionId; + } + + public static void disconnect(String connectionId) throws SQLException { + Connection connection = connections.get(connectionId); + if (connection != null && !connection.isClosed()) { + connection.close(); + } + connections.remove(connectionId); + connectionInfo.remove(connectionId); + } + + public static Connection getConnection(String connectionId) { + return connections.get(connectionId); + } + + public static DatabaseInfo getConnectionInfo(String connectionId) { + return connectionInfo.get(connectionId); + } + + private static String buildConnectionUrl(String driver, String host, String port, String database) { + String drv = driver == null ? "" : driver.toLowerCase(); + switch (drv) { + case "mysql": + return "jdbc:mysql://" + host + ":" + port + "/" + database; + case "postgresql": + return "jdbc:postgresql://" + host + ":" + port + "/" + database; + case "sqlite": + // 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠 + if (database == null || database.isEmpty()) { + return "jdbc:sqlite::memory:"; + } + String normalized = database.replace("\\", "/"); + // 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径 + if (!normalized.contains(":") && !normalized.startsWith("/")) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + normalized = userHome + "/" + normalized; + } + return "jdbc:sqlite:" + normalized; + case "oracle": + return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database; + case "h2": + // H2 文件路径同样做反斜杠处理 + if (database == null || database.isEmpty()) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db"; + } else { + String norm = database.replace("\\", "/"); + // 如果传入仅是名字(无斜杠或冒号),则存到用户目录下 + if (!norm.contains("/") && !norm.contains(":")) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + norm = userHome + "/.axis_innovators_box/databases/" + norm; + } + return "jdbc:h2:file:" + norm; + } + default: + throw new IllegalArgumentException("不支持的数据库类型: " + driver); + } + } + + + /** + * 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例)) + * @param driver mysql | postgresql | oracle + * @param host 数据库主机 + * @param port 端口 + * @param dbName 要创建的数据库名(或 schema 名) + * @param adminUser 管理员用户名(用于创建数据库) + * @param adminPassword 管理员密码 + * @return 如果创建成功返回一个简短消息,否则抛出 SQLException + * @throws SQLException + */ + public static String createDatabaseOnServer(String driver, String host, String port, + String dbName, String adminUser, String adminPassword) throws SQLException { + if (driver == null) throw new SQLException("driver 不能为空"); + String drv = driver.toLowerCase().trim(); + + // 简单校验 dbName(避免注入)——只允许字母数字下划线 + if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) { + throw new SQLException("不合法的数据库名: " + dbName); + } + + try { + switch (drv) { + case "mysql": + // 加载驱动(如果尚未加载) + try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { + throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e); + } + // 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE + String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC"; + try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"; + st.executeUpdate(sql); + } + return "MySQL 数据库创建成功: " + dbName; + + case "postgresql": + case "postgres": + try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) { + throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e); + } + // 连接到默认 postgres 数据库以创建新数据库 + String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres"; + try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'"; + st.executeUpdate(sql); + } + return "PostgreSQL 数据库创建成功: " + dbName; + + case "oracle": + // Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见) + try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { + throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e); + } + // 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName) + // 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID + String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID + try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + // 创建 user(schema)示例 + String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword + String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\""; + String grant = "GRANT CONNECT, RESOURCE TO " + dbName; + st.executeUpdate(createUser); + st.executeUpdate(grant); + } catch (SQLException ex) { + // Oracle 操作更容易失败,给出提示 + throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex); + } + return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName; + + default: + throw new SQLException("不支持的数据库类型: " + driver); + } + } catch (SQLException se) { + // 透传 SQLException,调用方会拿到 message 并反馈给前端 + throw se; + } catch (Exception e) { + throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e); + } + } + public static String createLocalDatabase(String driver, String dbName) throws SQLException { + switch (driver.toLowerCase()) { + case "sqlite": + // 创建目录并构造规范化路径(确保路径使用正斜杠) + String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db"); + java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); + try { + java.nio.file.Files.createDirectories(dbDir); + } catch (Exception e) { + throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); + } + String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/"); + + // 显式加载 sqlite 驱动(避免 No suitable driver) + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e); + } + + // 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path) + String connectionId = connect("sqlite", "", "", dbPath, "", ""); + + // 创建示例表 + createSampleTables(connectionId); + + return connectionId; + + case "h2": + java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); + try { + java.nio.file.Files.createDirectories(h2Dir); + } catch (Exception e) { + throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); + } + String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/"); + String h2Url = "jdbc:h2:file:" + h2Path; + + try { + Class.forName("org.h2.Driver"); + } catch (ClassNotFoundException e) { + throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e); + } + + Connection h2Conn = DriverManager.getConnection(h2Url, "sa", ""); + String h2ConnectionId = "conn_" + System.currentTimeMillis(); + connections.put(h2ConnectionId, h2Conn); + connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa")); + + createSampleTables(h2ConnectionId); + + return h2ConnectionId; + + default: + throw new SQLException("不支持创建本地数据库类型: " + driver); + } + } + + private static void createSampleTables(String connectionId) throws SQLException { + Connection conn = getConnection(connectionId); + DatabaseInfo info = getConnectionInfo(connectionId); + + if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) { + try (Statement stmt = conn.createStatement()) { + // 创建用户表 + stmt.execute("CREATE TABLE IF NOT EXISTS users (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "username VARCHAR(50) NOT NULL UNIQUE, " + + "email VARCHAR(100) NOT NULL, " + + "password VARCHAR(100) NOT NULL, " + + "status VARCHAR(20) DEFAULT 'active', " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + + ")"); + + // 创建产品表 + stmt.execute("CREATE TABLE IF NOT EXISTS products (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "name VARCHAR(100) NOT NULL, " + + "description TEXT, " + + "price DECIMAL(10,2) NOT NULL, " + + "stock INTEGER DEFAULT 0, " + + "category VARCHAR(50), " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + + ")"); + + // 创建订单表 + stmt.execute("CREATE TABLE IF NOT EXISTS orders (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "user_id INTEGER, " + + "product_id INTEGER, " + + "quantity INTEGER NOT NULL, " + + "total_price DECIMAL(10,2) NOT NULL, " + + "status VARCHAR(20) DEFAULT 'pending', " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (user_id) REFERENCES users(id), " + + "FOREIGN KEY (product_id) REFERENCES products(id)" + + ")"); + + // 插入示例数据 + insertSampleData(conn); + } + } + } + + private static void insertSampleData(Connection conn) throws SQLException { + // 检查是否已有数据 + try (Statement checkStmt = conn.createStatement(); + ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) { + if (rs.next() && rs.getInt(1) == 0) { + // 插入用户数据 + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) { + String[][] users = { + {"admin", "admin@example.com", "password123"}, + {"user1", "user1@example.com", "password123"}, + {"user2", "user2@example.com", "password123"} + }; + + for (String[] user : users) { + pstmt.setString(1, user[0]); + pstmt.setString(2, user[1]); + pstmt.setString(3, user[2]); + pstmt.executeUpdate(); + } + } + + // 插入产品数据 + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) { + Object[][] products = { + {"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"}, + {"智能手机", "最新款智能手机", 3999.99, 100, "电子"}, + {"办公椅", "舒适办公椅", 299.99, 30, "家居"}, + {"咖啡机", "全自动咖啡机", 899.99, 20, "家电"} + }; + + for (Object[] product : products) { + pstmt.setString(1, (String) product[0]); + pstmt.setString(2, (String) product[1]); + pstmt.setDouble(3, (Double) product[2]); + pstmt.setInt(4, (Integer) product[3]); + pstmt.setString(5, (String) product[4]); + pstmt.executeUpdate(); + } + } + } + } + } +} diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java index 2f60d8d..831fc27 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java @@ -43,32 +43,33 @@ public class RegistrationSettingsItem extends WindowsJDialog { private final AxisInnovatorsBox mainWindow; static { - RegistrationSettingsItem registrationSettingsItem = new RegistrationSettingsItem(); - JPanel pluginPanel = createPluginSettingsPanel(); - JPanel generalPanel = createGeneralSettingsPanel(); - JPanel aboutPanel = createAboutPanel(); - JPanel themePanel = createThemePanel(); - - registrationSettingsItem.addSettings( - generalPanel, language.getText("settings.2.title"), - null, language.getText("settings.2.tip"), "system:settings_appearance_item" - ); - registrationSettingsItem.addSettings( - pluginPanel, language.getText("settings.1.title"), - null, language.getText("settings.1.tip"), "system:settings_plugins_item" - ); - registrationSettingsItem.addSettings( - themePanel, language.getText("settings.4.title"), - null, language.getText("settings.4.tip"), "system:settings_theme_item" - ); - registrationSettingsItem.addSettings( - aboutPanel, language.getText("settings.3.title"), - null, language.getText("settings.3.tip"), "system:settings_information_item" - ); - - registrationSettingsItemList.add( - registrationSettingsItem - ); + //RegistrationSettingsItem registrationSettingsItem = new RegistrationSettingsItem(); + //JPanel pluginPanel = createPluginSettingsPanel(); + //JPanel generalPanel = createGeneralSettingsPanel(); + //JPanel aboutPanel = createAboutPanel(); + //JPanel themePanel = createThemePanel(); +// + //registrationSettingsItem.addSettings( + // generalPanel, language.getText("settings.2.title"), + // null, language.getText("settings.2.tip"), "system:settings_appearance_item" + //); + //registrationSettingsItem.addSettings( + // pluginPanel, language.getText("settings.1.title"), + // null, language.getText("settings.1.tip"), "system:settings_plugins_item" + //); + //registrationSettingsItem.addSettings( + // themePanel, language.getText("settings.4.title"), + // null, language.getText("settings.4.tip"), "system:settings_theme_item" + //); + //registrationSettingsItem.addSettings( + // aboutPanel, language.getText("settings.3.title"), + // null, language.getText("settings.3.tip"), "system:settings_information_item" + //); +// + //registrationSettingsItemList.add( + // registrationSettingsItem + //); + overloading(); } public static void overloading() { @@ -624,7 +625,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { MainWindow mainWindow = getMainWindow(); if (mainWindow != null) { mainWindow.setBackgroundWithGlassEffect(bgImage, blurAmount,1.0f); - logger.info("图片背景已应用,模糊度: " + blurAmount); + logger.info("图片背景已应用,模糊度: {}", blurAmount); // 保存设置到 StateManager String backgroundPath = (String) backgroundPreview.getClientProperty("backgroundPath"); @@ -957,7 +958,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { settingsManager.saveState("background.path", backgroundPath); settingsManager.saveState("background.blur", blurAmount); settingsManager.saveState("background.enabled", true); - logger.info("背景设置已保存: " + backgroundPath + ", 模糊度: " + blurAmount); + logger.info("背景设置已保存: {}, 模糊度: {}", backgroundPath, blurAmount); } } diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java index 4ad99a4..e910bbe 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java @@ -82,6 +82,16 @@ public class RegistrationTool { } })); + programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png", + "用于管理数据库" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupDataBaseWindow(); + } + })); + MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具", "ai/ai.png", "人工智能/大语言模型"); diff --git a/src/main/java/com/axis/innovators/box/window/MainWindow.java b/src/main/java/com/axis/innovators/box/window/MainWindow.java index 05dd1c6..be796e6 100644 --- a/src/main/java/com/axis/innovators/box/window/MainWindow.java +++ b/src/main/java/com/axis/innovators/box/window/MainWindow.java @@ -938,7 +938,15 @@ public class MainWindow extends JFrame { // ---------- 工具卡/面板 ---------- private JPanel createToolsPanel(ToolCategory category) { - JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)); + JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)) { + @Override + public Dimension getPreferredSize() { + // 计算容器宽度以恰好容纳3个卡片和间隙 + int cardWidth = 240; // 卡片宽度 + int gap = 16; + return new Dimension(cardWidth * 3 + gap * 2, super.getPreferredSize().height); + } + }; panel.setOpaque(false); panel.setBorder(null); diff --git a/src/main/resources/icons/programming/JarApiViewer/database_dark.png b/src/main/resources/icons/programming/JarApiViewer/database_dark.png new file mode 100644 index 0000000..434579c Binary files /dev/null and b/src/main/resources/icons/programming/JarApiViewer/database_dark.png differ diff --git a/src/main/resources/icons/programming/database.png b/src/main/resources/icons/programming/database.png new file mode 100644 index 0000000..64ecbc9 Binary files /dev/null and b/src/main/resources/icons/programming/database.png differ diff --git a/src/main/resources/icons/programming/database_dark.png b/src/main/resources/icons/programming/database_dark.png new file mode 100644 index 0000000..434579c Binary files /dev/null and b/src/main/resources/icons/programming/database_dark.png differ