From 48427bb42e5f0ac83a1a17a666ba68115f8222bc Mon Sep 17 00:00:00 2001 From: slankka Date: Mon, 31 Dec 2018 03:52:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0OverSSH=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=BF=9E=E6=8E=A5=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../bridge/MybatisGeneratorBridge.java | 3 +- .../controller/DbConnectionController.java | 181 +++++++---------- .../generator/controller/FXMLPage.java | 3 +- .../controller/MainUIController.java | 32 ++- .../controller/OverSshController.java | 190 ++++++++++++++++++ .../PictureProcessStateController.java | 134 ++++++++++++ .../controller/TabPaneController.java | 169 ++++++++++++++++ .../generator/model/DatabaseConfig.java | 101 +++++++++- .../mybatis/generator/util/ConfigHelper.java | 4 +- .../zzg/mybatis/generator/util/DbUtil.java | 108 +++++++++- src/main/resources/fxml/basicConnection.fxml | 79 ++++++++ src/main/resources/fxml/newConnection.fxml | 145 ++++++------- .../resources/fxml/sshBasedConnection.fxml | 148 ++++++++++++++ src/main/resources/icons/SSH_tunnel.png | Bin 0 -> 20758 bytes .../icons/SSH_tunnel_disconnected.png | Bin 0 -> 21023 bytes 16 files changed, 1092 insertions(+), 210 deletions(-) create mode 100644 src/main/java/com/zzg/mybatis/generator/controller/OverSshController.java create mode 100644 src/main/java/com/zzg/mybatis/generator/controller/PictureProcessStateController.java create mode 100644 src/main/java/com/zzg/mybatis/generator/controller/TabPaneController.java create mode 100644 src/main/resources/fxml/basicConnection.fxml create mode 100644 src/main/resources/fxml/sshBasedConnection.fxml create mode 100644 src/main/resources/icons/SSH_tunnel.png create mode 100644 src/main/resources/icons/SSH_tunnel_disconnected.png diff --git a/pom.xml b/pom.xml index b848c50f..926ba567 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,11 @@ slf4j-api 1.7.25 + + com.jcraft + jsch + 0.1.54 + diff --git a/src/main/java/com/zzg/mybatis/generator/bridge/MybatisGeneratorBridge.java b/src/main/java/com/zzg/mybatis/generator/bridge/MybatisGeneratorBridge.java index f6363dca..ec38bd6f 100644 --- a/src/main/java/com/zzg/mybatis/generator/bridge/MybatisGeneratorBridge.java +++ b/src/main/java/com/zzg/mybatis/generator/bridge/MybatisGeneratorBridge.java @@ -1,5 +1,7 @@ package com.zzg.mybatis.generator.bridge; +import com.jcraft.jsch.Session; +import com.zzg.mybatis.generator.controller.PictureProcessStateController; import com.zzg.mybatis.generator.model.DatabaseConfig; import com.zzg.mybatis.generator.model.DbType; import com.zzg.mybatis.generator.model.GeneratorConfig; @@ -259,7 +261,6 @@ public void generate() throws Exception { mappingXMLFile.delete(); } } - myBatisGenerator.generate(progressCallback, contexts, fullyqualifiedTables); } diff --git a/src/main/java/com/zzg/mybatis/generator/controller/DbConnectionController.java b/src/main/java/com/zzg/mybatis/generator/controller/DbConnectionController.java index 18331bd9..358d997a 100644 --- a/src/main/java/com/zzg/mybatis/generator/controller/DbConnectionController.java +++ b/src/main/java/com/zzg/mybatis/generator/controller/DbConnectionController.java @@ -1,9 +1,7 @@ package com.zzg.mybatis.generator.controller; -import com.zzg.mybatis.generator.exception.DbDriverLoadingException; import com.zzg.mybatis.generator.model.DatabaseConfig; import com.zzg.mybatis.generator.util.ConfigHelper; -import com.zzg.mybatis.generator.util.DbUtil; import com.zzg.mybatis.generator.view.AlertUtil; import javafx.fxml.FXML; import javafx.scene.control.ChoiceBox; @@ -17,113 +15,88 @@ public class DbConnectionController extends BaseFXController { - private static final Logger _LOG = LoggerFactory.getLogger(DbConnectionController.class); + private static final Logger _LOG = LoggerFactory.getLogger(DbConnectionController.class); - @FXML - private TextField nameField; - @FXML - private TextField hostField; - @FXML - private TextField portField; - @FXML - private TextField userNameField; - @FXML - private TextField passwordField; - @FXML - private TextField schemaField; - @FXML - private ChoiceBox encodingChoice; - @FXML - private ChoiceBox dbTypeChoice; - private MainUIController mainUIController; - private boolean isUpdate = false; - private Integer primayKey; + @FXML + protected TextField nameField; + @FXML + protected TextField hostField; + @FXML + protected TextField portField; + @FXML + protected TextField userNameField; + @FXML + protected TextField passwordField; + @FXML + protected TextField schemaField; + @FXML + protected ChoiceBox encodingChoice; + @FXML + protected ChoiceBox dbTypeChoice; + protected MainUIController mainUIController; + protected boolean isUpdate = false; + protected Integer primayKey; + @Override + public void initialize(URL location, ResourceBundle resources) { + } - @Override - public void initialize(URL location, ResourceBundle resources) { - } + final void saveConnection() { + DatabaseConfig config = extractConfigForUI(); + if (config == null) { + return; + } + try { + ConfigHelper.saveDatabaseConfig(this.isUpdate, primayKey, config); + getDialogStage().close(); + mainUIController.loadLeftDBTree(); + } catch (Exception e) { + _LOG.error(e.getMessage(), e); + AlertUtil.showErrorAlert(e.getMessage()); + } + } - @FXML - void saveConnection() { - DatabaseConfig config = extractConfigForUI(); - if (config == null) { - return; - } - try { - ConfigHelper.saveDatabaseConfig(this.isUpdate, primayKey, config); - getDialogStage().close(); - mainUIController.loadLeftDBTree(); - } catch (Exception e) { - _LOG.error(e.getMessage(), e); - AlertUtil.showErrorAlert(e.getMessage()); - } - } + void setMainUIController(MainUIController controller) { + this.mainUIController = controller; + super.setDialogStage(mainUIController.getDialogStage()); + } - @FXML - void testConnection() { - DatabaseConfig config = extractConfigForUI(); - if (config == null) { - return; - } - try { - DbUtil.getConnection(config); - AlertUtil.showInfoAlert("连接成功"); - } catch (DbDriverLoadingException e){ - _LOG.error("{}", e); - AlertUtil.showWarnAlert("连接失败, "+e.getMessage()); - } catch (Exception e) { - _LOG.error(e.getMessage(), e); - AlertUtil.showWarnAlert("连接失败"); - } + public DatabaseConfig extractConfigForUI() { + String name = nameField.getText(); + String host = hostField.getText(); + String port = portField.getText(); + String userName = userNameField.getText(); + String password = passwordField.getText(); + String encoding = encodingChoice.getValue(); + String dbType = dbTypeChoice.getValue(); + String schema = schemaField.getText(); + DatabaseConfig config = new DatabaseConfig(); + config.setName(name); + config.setDbType(dbType); + config.setHost(host); + config.setPort(port); + config.setUsername(userName); + config.setPassword(password); + config.setSchema(schema); + config.setEncoding(encoding); + if (StringUtils.isAnyEmpty(name, host, port, userName, encoding, dbType, schema)) { + AlertUtil.showWarnAlert("密码以外其他字段必填"); + return null; + } + return config; + } - } - - @FXML - void cancel() { - getDialogStage().close(); - } - - void setMainUIController(MainUIController controller) { - this.mainUIController = controller; - } - - private DatabaseConfig extractConfigForUI() { - String name = nameField.getText(); - String host = hostField.getText(); - String port = portField.getText(); - String userName = userNameField.getText(); - String password = passwordField.getText(); - String encoding = encodingChoice.getValue(); - String dbType = dbTypeChoice.getValue(); - String schema = schemaField.getText(); - DatabaseConfig config = new DatabaseConfig(); - config.setName(name); - config.setDbType(dbType); - config.setHost(host); - config.setPort(port); - config.setUsername(userName); - config.setPassword(password); - config.setSchema(schema); - config.setEncoding(encoding); - if (StringUtils.isAnyEmpty(name, host, port, userName, encoding, dbType, schema)) { - AlertUtil.showWarnAlert("密码以外其他字段必填"); - return null; - } - return config; - } - - public void setConfig(DatabaseConfig config) { - isUpdate = true; - primayKey = config.getId(); // save id for update config - nameField.setText(config.getName()); - hostField.setText(config.getHost()); - portField.setText(config.getPort()); - userNameField.setText(config.getUsername()); - passwordField.setText(config.getPassword()); - encodingChoice.setValue(config.getEncoding()); - dbTypeChoice.setValue(config.getDbType()); - schemaField.setText(config.getSchema()); - } + public void setConfig(DatabaseConfig config) { + isUpdate = true; + primayKey = config.getId(); // save id for update config + nameField.setText(config.getName()); + hostField.setText(config.getHost()); + portField.setText(config.getPort()); + userNameField.setText(config.getUsername()); + passwordField.setText(config.getPassword()); + encodingChoice.setValue(config.getEncoding()); + dbTypeChoice.setValue(config.getDbType()); + schemaField.setText(config.getSchema()); + } } diff --git a/src/main/java/com/zzg/mybatis/generator/controller/FXMLPage.java b/src/main/java/com/zzg/mybatis/generator/controller/FXMLPage.java index 65f7876a..796bed5f 100644 --- a/src/main/java/com/zzg/mybatis/generator/controller/FXMLPage.java +++ b/src/main/java/com/zzg/mybatis/generator/controller/FXMLPage.java @@ -9,7 +9,8 @@ public enum FXMLPage { NEW_CONNECTION("fxml/newConnection.fxml"), SELECT_TABLE_COLUMN("fxml/selectTableColumn.fxml"), - GENERATOR_CONFIG("fxml/generatorConfigs.fxml"),; + GENERATOR_CONFIG("fxml/generatorConfigs.fxml"), + ; private String fxml; diff --git a/src/main/java/com/zzg/mybatis/generator/controller/MainUIController.java b/src/main/java/com/zzg/mybatis/generator/controller/MainUIController.java index 4689d10a..af06bac0 100644 --- a/src/main/java/com/zzg/mybatis/generator/controller/MainUIController.java +++ b/src/main/java/com/zzg/mybatis/generator/controller/MainUIController.java @@ -1,5 +1,6 @@ package com.zzg.mybatis.generator.controller; +import com.jcraft.jsch.Session; import com.zzg.mybatis.generator.bridge.MybatisGeneratorBridge; import com.zzg.mybatis.generator.model.DatabaseConfig; import com.zzg.mybatis.generator.model.GeneratorConfig; @@ -11,6 +12,7 @@ import com.zzg.mybatis.generator.view.UIProgressCallback; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.control.Label; @@ -117,7 +119,7 @@ public void initialize(URL location, ResourceBundle resources) { dbImage.setFitWidth(40); connectionLabel.setGraphic(dbImage); connectionLabel.setOnMouseClicked(event -> { - DbConnectionController controller = (DbConnectionController) loadFXMLPage("新建数据库连接", FXMLPage.NEW_CONNECTION, false); + TabPaneController controller = (TabPaneController) loadFXMLPage("新建数据库连接", FXMLPage.NEW_CONNECTION, false); controller.setMainUIController(this); controller.showDialogStage(); }); @@ -154,7 +156,7 @@ public void initialize(URL location, ResourceBundle resources) { MenuItem item2 = new MenuItem("编辑连接"); item2.setOnAction(event1 -> { DatabaseConfig selectedConfig = (DatabaseConfig) treeItem.getGraphic().getUserData(); - DbConnectionController controller = (DbConnectionController) loadFXMLPage("编辑数据库连接", FXMLPage.NEW_CONNECTION, false); + TabPaneController controller = (TabPaneController) loadFXMLPage("编辑数据库连接", FXMLPage.NEW_CONNECTION, false); controller.setMainUIController(this); controller.setConfig(selectedConfig); controller.showDialogStage(); @@ -178,7 +180,6 @@ public void initialize(URL location, ResourceBundle resources) { } treeItem.setExpanded(true); if (level == 1) { - System.out.println("index: " + leftDBTree.getSelectionModel().getSelectedIndex()); DatabaseConfig selectedConfig = (DatabaseConfig) treeItem.getGraphic().getUserData(); try { List tables = DbUtil.getTableNames(selectedConfig); @@ -287,7 +288,32 @@ public void generateCode() { bridge.setProgressCallback(alert); alert.show(); try { + //Engage PortForwarding + Session sshSession = DbUtil.getSSHSession(selectedDatabaseConfig); + DbUtil.engagePortForwarding(sshSession, selectedDatabaseConfig); + PictureProcessStateController pictureProcessStateController = null; + if (sshSession != null) { + pictureProcessStateController = new PictureProcessStateController(); + pictureProcessStateController.setDialogStage(getDialogStage()); + pictureProcessStateController.startPlay(); + } + bridge.generate(); + + if (pictureProcessStateController != null) { + Task task = new Task() { + @Override + protected Void call() throws Exception { + Thread.sleep(3000); + return null; + } + }; + PictureProcessStateController finalPictureProcessStateController = pictureProcessStateController; + task.setOnSucceeded(event -> { + finalPictureProcessStateController.close(); + }); + new Thread(task).start(); + } } catch (Exception e) { e.printStackTrace(); AlertUtil.showErrorAlert(e.getMessage()); diff --git a/src/main/java/com/zzg/mybatis/generator/controller/OverSshController.java b/src/main/java/com/zzg/mybatis/generator/controller/OverSshController.java new file mode 100644 index 00000000..9231a873 --- /dev/null +++ b/src/main/java/com/zzg/mybatis/generator/controller/OverSshController.java @@ -0,0 +1,190 @@ +package com.zzg.mybatis.generator.controller; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.zzg.mybatis.generator.model.DatabaseConfig; +import com.zzg.mybatis.generator.util.ConfigHelper; +import com.zzg.mybatis.generator.util.DbUtil; +import com.zzg.mybatis.generator.view.AlertUtil; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.paint.Paint; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.*; + +/** + * Project: mybatis-generator-gui + * + * @author slankka on 2018/12/30. + */ +public class OverSshController extends DbConnectionController { + private Logger logger = LoggerFactory.getLogger(OverSshController.class); + + @FXML + public Label lPortLabel; + @FXML + public TextField sshUserField; + @FXML + public PasswordField sshPasswordField; + @FXML + private TextField sshHostField; + @FXML + private TextField sshdPortField; + @FXML + private TextField lportField; + @FXML + private TextField rportField; + @FXML + private Label note; + + @Override + public void initialize(URL location, ResourceBundle resources) { + } + + public void setDbConnectionConfig(DatabaseConfig databaseConfig) { + if (databaseConfig == null) { + return; + } + isUpdate = true; + super.setConfig(databaseConfig); + this.sshdPortField.setText(databaseConfig.getSshPort()); + this.sshHostField.setText(databaseConfig.getSshHost()); + this.lportField.setText(databaseConfig.getLport()); + this.rportField.setText(databaseConfig.getRport()); + this.sshUserField.setText(databaseConfig.getSshUser()); + this.sshPasswordField.setText(databaseConfig.getSshPassword()); + //例如:默认从本机的 3306 -> 转发到 3306 + if (StringUtils.isBlank(this.lportField.getText())) { + this.lportField.setText(databaseConfig.getPort()); + } + if (StringUtils.isBlank(this.rportField.getText())) { + this.rportField.setText(databaseConfig.getPort()); + } + checkInput(); + } + + @FXML + public void checkInput() { + DatabaseConfig databaseConfig = extractConfigFromUi(); + if (StringUtils.isBlank(databaseConfig.getSshHost()) + || StringUtils.isBlank(databaseConfig.getSshPort()) + || StringUtils.isBlank(databaseConfig.getSshUser()) + || StringUtils.isBlank(databaseConfig.getSshPassword()) + ) { + note.setText("当前SSH配置输入不完整,OVER SSH不生效"); + note.setTextFill(Paint.valueOf("#ff666f")); + } else { + note.setText("当前SSH配置生效"); + note.setTextFill(Paint.valueOf("#5da355")); + } + } + + public void setLPortLabelText(String text) { + lPortLabel.setText(text); + } + + public void recoverNotice() { + this.lPortLabel.setText("注意不要填写被其他程序占用的端口"); + } + + public DatabaseConfig extractConfigFromUi() { + String name = nameField.getText(); + String host = hostField.getText(); + String port = portField.getText(); + String userName = userNameField.getText(); + String password = passwordField.getText(); + String encoding = encodingChoice.getValue(); + String dbType = dbTypeChoice.getValue(); + String schema = schemaField.getText(); + DatabaseConfig config = new DatabaseConfig(); + config.setName(name); + config.setDbType(dbType); + config.setHost(host); + config.setPort(port); + config.setUsername(userName); + config.setPassword(password); + config.setSchema(schema); + config.setEncoding(encoding); + config.setSshHost(this.sshHostField.getText()); + config.setSshPort(this.sshdPortField.getText()); + config.setLport(this.lportField.getText()); + config.setRport(this.rportField.getText()); + config.setSshUser(this.sshUserField.getText()); + config.setSshPassword(this.sshPasswordField.getText()); + return config; + } + + public void saveConfig() { + DatabaseConfig databaseConfig = extractConfigFromUi(); + if (StringUtils.isAnyEmpty( + databaseConfig.getName(), + databaseConfig.getHost(), + databaseConfig.getPort(), + databaseConfig.getUsername(), + databaseConfig.getEncoding(), + databaseConfig.getDbType(), + databaseConfig.getSchema())) { + AlertUtil.showWarnAlert("密码以外其他字段必填"); + return; + } + try { + ConfigHelper.saveDatabaseConfig(this.isUpdate, primayKey, databaseConfig); + getDialogStage().close(); + mainUIController.loadLeftDBTree(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + AlertUtil.showErrorAlert(e.getMessage()); + } + } + + @FXML + public void testSSH() { + Session session = DbUtil.getSSHSession(extractConfigFromUi()); + if (session == null) { + AlertUtil.showErrorAlert("请检查主机,端口,用户名,以及密码是否填写正确"); + return; + } + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { + try { + session.connect(); + } catch (JSchException e) { + logger.error("Connect Over SSH failed", e); + throw new RuntimeException(e.getMessage()); + } + }); + executorService.shutdown(); + try { + boolean b = executorService.awaitTermination(5, TimeUnit.SECONDS); + if (!b) { + throw new TimeoutException("连接超时"); + } + result.get(); + AlertUtil.showInfoAlert("连接SSH服务器成功,恭喜你可以使用OverSSH功能"); + recoverNotice(); + } catch (Exception e) { + AlertUtil.showErrorAlert("请检查主机,端口,用户名,以及密码是否填写正确: " + e.getMessage()); + } finally { + DbUtil.shutdownPortForwarding(session); + } + } + + @FXML + public void reset(ActionEvent actionEvent) { + this.sshUserField.clear(); + this.sshPasswordField.clear(); + this.sshdPortField.clear(); + this.sshHostField.clear(); + this.lportField.clear(); + this.rportField.clear(); + recoverNotice(); + } +} diff --git a/src/main/java/com/zzg/mybatis/generator/controller/PictureProcessStateController.java b/src/main/java/com/zzg/mybatis/generator/controller/PictureProcessStateController.java new file mode 100644 index 00000000..e6f792d8 --- /dev/null +++ b/src/main/java/com/zzg/mybatis/generator/controller/PictureProcessStateController.java @@ -0,0 +1,134 @@ +package com.zzg.mybatis.generator.controller; + +import javafx.animation.RotateTransition; +import javafx.animation.Timeline; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + + +import static javafx.scene.paint.Color.DARKSEAGREEN; + +public class PictureProcessStateController { + private ImageView dbImage = new ImageView("icons/SSH_tunnel.png"); + private final Rectangle rect = new Rectangle(20, 20, 30, 30); + private final RotateTransition rotateTransition = new RotateTransition(); + private final Text text = new Text(""); + private final Stage dialogStage = new Stage(StageStyle.TRANSPARENT); + private double initX; + private double initY; + private Stage parentStage; + private final Button button = new Button(""); + public void setDialogStage(Stage stage) { + this.parentStage = stage; + } + + public void startPlay() { + dbImage.setFitHeight(192); + dbImage.setFitWidth(798); + Group rootGroup = new Group(); + Scene scene = new Scene(rootGroup, 800, 212, Color.TRANSPARENT); + dialogStage.initModality(Modality.APPLICATION_MODAL); + dialogStage.setScene(scene); + dialogStage.initOwner(parentStage); + dialogStage.centerOnScreen(); + dialogStage.setTitle("OverSSH"); + + rect.setArcHeight(10); + rect.setArcWidth(10); + rect.setFill(DARKSEAGREEN); + + rotateTransition.setNode(rect); + rotateTransition.setDuration(Duration.seconds(0.8d)); + rotateTransition.setFromAngle(0); + rotateTransition.setToAngle(720); + rotateTransition.setCycleCount(Timeline.INDEFINITE); + rotateTransition.setAutoReverse(true); + VBox vBoxRect = new VBox(); + vBoxRect.setAlignment(Pos.TOP_CENTER); + vBoxRect.getChildren().add(rect); + VBox.setMargin(rect, new Insets(125, 0, 0, 350)); + rotateTransition.play(); + + + text.setFont(Font.font(12)); + VBox vBoxLabel = new VBox(); + vBoxLabel.getChildren().add(text); + VBox.setMargin(text, new Insets(175, 0, 15, 40)); + + + button.setPrefSize(90, 40); + HBox hBoxButton = new HBox(); + hBoxButton.setPrefSize(505, 170); + hBoxButton.getChildren().add(button); + hBoxButton.setAlignment(Pos.BOTTOM_RIGHT); + hBoxButton.getStylesheets().add(Thread.currentThread().getContextClassLoader().getResource("style.css").toExternalForm()); + HBox.setMargin(button, new Insets(0, 15, 5, 0)); + button.setStyle("-fx-border-width: 0px;"); + button.setStyle("-fx-border-color: transparent;"); + button.setStyle("-fx-background-color: transparent;"); + rootGroup.getChildren().addAll(dbImage, vBoxRect, vBoxLabel, hBoxButton); + dialogStage.show(); + + button.setOnMouseClicked((event) -> dialogStage.close()); + + rootGroup.setOnMousePressed((me) -> { + initX = me.getScreenX() - dialogStage.getX(); + initY = me.getScreenY() - dialogStage.getY(); + }); + rootGroup.setOnMouseDragged((me) -> { + dialogStage.setX(me.getScreenX() - initX); + dialogStage.setY(me.getScreenY() - initY); + }); + } + + public void playFailState(String message, boolean showButton) { + rect.setFill(Color.ORANGERED); + rotateTransition.stop(); + dbImage.setImage(new Image("icons/SSH_tunnel_disconnected.png")); + rotateTransition.setDuration(Duration.seconds(3)); + rotateTransition.play(); + text.setText(message); + if (showButton) { + showCloseButton(); + } + } + + private void showCloseButton() { + button.getStyleClass().add("btn"); + button.getStyleClass().add("btn-default"); + button.setStyle("-fx-border-width: 1px;"); + button.setStyle("-fx-background-color: #fff;"); + button.setText("我知道了"); + } + + public void playSuccessState(String message, boolean showButton) { + rect.setFill(DARKSEAGREEN); + rotateTransition.stop(); + dbImage.setImage(new Image("icons/SSH_tunnel.png")); + rotateTransition.setDuration(Duration.seconds(0.8)); + rotateTransition.play(); + text.setText(message); + if (showButton) { + showCloseButton(); + } + } + + public void close() { + dialogStage.close(); + } +} diff --git a/src/main/java/com/zzg/mybatis/generator/controller/TabPaneController.java b/src/main/java/com/zzg/mybatis/generator/controller/TabPaneController.java new file mode 100644 index 00000000..766ad53a --- /dev/null +++ b/src/main/java/com/zzg/mybatis/generator/controller/TabPaneController.java @@ -0,0 +1,169 @@ +package com.zzg.mybatis.generator.controller; + +import com.jcraft.jsch.Session; +import com.zzg.mybatis.generator.model.DatabaseConfig; +import com.zzg.mybatis.generator.util.DbUtil; +import com.zzg.mybatis.generator.view.AlertUtil; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.TabPane; +import javafx.scene.layout.AnchorPane; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.EOFException; +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Project: mybatis-generator-gui + * + * @author github.com/slankka on 2019/1/22. + */ +public class TabPaneController extends BaseFXController { + private static Logger logger = LoggerFactory.getLogger(TabPaneController.class); + + @FXML + private TabPane tabPane; + + @FXML + private DbConnectionController tabControlAController; + + @FXML + private OverSshController tabControlBController; + + private boolean isOverssh; + + private MainUIController mainUIController; + + @Override + public void initialize(URL location, ResourceBundle resources) { + tabPane.setPrefHeight(((AnchorPane) tabPane.getSelectionModel().getSelectedItem().getContent()).getPrefHeight()); + tabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + isOverssh = observable.getValue().getText().equals("SSH"); + tabPane.prefHeightProperty().bind(((AnchorPane) tabPane.getSelectionModel().getSelectedItem().getContent()).prefHeightProperty()); + getDialogStage().close(); + getDialogStage().show(); + }); + } + + public void setMainUIController(MainUIController mainUIController) { + this.mainUIController = mainUIController; + this.tabControlAController.setMainUIController(mainUIController); + this.tabControlBController.setMainUIController(mainUIController); + } + + public void setConfig(DatabaseConfig selectedConfig) { + tabControlAController.setConfig(selectedConfig); + tabControlBController.setDbConnectionConfig(selectedConfig); + if (StringUtils.isNoneBlank( + selectedConfig.getSshHost(), + selectedConfig.getSshPassword(), + selectedConfig.getSshPort(), + selectedConfig.getSshUser(), + selectedConfig.getLport())) { + logger.info("Found SSH based Config"); + tabPane.getSelectionModel().selectLast(); + } + } + + private DatabaseConfig extractConfigForUI() { + if (isOverssh) { + return tabControlBController.extractConfigFromUi(); + } else { + return tabControlAController.extractConfigForUI(); + } + } + + @FXML + void saveConnection() { + if (isOverssh) { + tabControlBController.saveConfig(); + } else { + tabControlAController.saveConnection(); + } + } + + + @FXML + void testConnection() { + DatabaseConfig config = extractConfigForUI(); + if (config == null) { + return; + } + if (StringUtils.isAnyEmpty(config.getName(), + config.getHost(), + config.getPort(), + config.getUsername(), + config.getEncoding(), + config.getDbType(), + config.getSchema())) { + AlertUtil.showWarnAlert("密码以外其他字段必填"); + return; + } + Session sshSession = DbUtil.getSSHSession(config); + if (isOverssh && sshSession != null) { + PictureProcessStateController pictureProcessState = new PictureProcessStateController(); + pictureProcessState.setDialogStage(getDialogStage()); + pictureProcessState.startPlay(); + //如果不用异步,则视图会等方法返回才会显示 + Task task = new Task() { + @Override + protected Void call() throws Exception { + DbUtil.engagePortForwarding(sshSession, config); + DbUtil.getConnection(config); + return null; + } + }; + task.setOnFailed(event -> { + Throwable e = task.getException(); + logger.error("task Failed", e); + if (e instanceof RuntimeException) { + if (e.getMessage().equals("Address already in use: JVM_Bind")) { + tabControlBController.setLPortLabelText(config.getLport() + "已经被占用,请换其他端口"); + } + //端口转发一定不成功,导致数据库连接不上 + pictureProcessState.playFailState("连接失败:" + e.getMessage(), true); + return; + } + + if (e.getCause() instanceof EOFException) { + pictureProcessState.playFailState("连接失败, 请检查数据库的主机名,并且检查端口和目标端口是否一致", true); + //端口转发已经成功,但是数据库连接不上,故需要释放连接 + DbUtil.shutdownPortForwarding(sshSession); + return; + } + pictureProcessState.playFailState("连接失败:" + e.getMessage(), true); + //可能是端口转发已经成功,但是数据库连接不上,故需要释放连接 + DbUtil.shutdownPortForwarding(sshSession); + }); + task.setOnSucceeded(event -> { + try { + pictureProcessState.playSuccessState("连接成功", true); + DbUtil.shutdownPortForwarding(sshSession); + tabControlBController.recoverNotice(); + } catch (Exception e) { + logger.error("", e); + } + }); + new Thread(task).start(); + } else { + try { + DbUtil.getConnection(config); + AlertUtil.showInfoAlert("连接成功"); + } catch (RuntimeException e) { + logger.error("", e); + AlertUtil.showWarnAlert("连接失败, " + e.getMessage()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + AlertUtil.showWarnAlert("连接失败"); + } + } + } + + @FXML + void cancel() { + getDialogStage().close(); + } +} diff --git a/src/main/java/com/zzg/mybatis/generator/model/DatabaseConfig.java b/src/main/java/com/zzg/mybatis/generator/model/DatabaseConfig.java index e0fc7f94..951380e9 100644 --- a/src/main/java/com/zzg/mybatis/generator/model/DatabaseConfig.java +++ b/src/main/java/com/zzg/mybatis/generator/model/DatabaseConfig.java @@ -30,6 +30,18 @@ public class DatabaseConfig { private String encoding; + private String lport; + + private String rport; + + private String sshPort; + + private String sshHost; + + private String sshUser; + + private String sshPassword; + public Integer getId() { return id; } @@ -102,26 +114,99 @@ public void setDbType(String dbType) { this.dbType = dbType; } + public String getLport() { + return lport; + } + + public void setLport(String lport) { + this.lport = lport; + } + + public String getRport() { + return rport; + } + + public void setRport(String rport) { + this.rport = rport; + } + + public String getSshPort() { + return sshPort; + } + + public void setSshPort(String sshPort) { + this.sshPort = sshPort; + } + + public String getSshHost() { + return sshHost; + } + + public void setSshHost(String sshHost) { + this.sshHost = sshHost; + } + + public String getSshUser() { + return sshUser; + } + + public void setSshUser(String sshUser) { + this.sshUser = sshUser; + } + + public String getSshPassword() { + return sshPassword; + } + + public void setSshPassword(String sshPassword) { + this.sshPassword = sshPassword; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DatabaseConfig that = (DatabaseConfig) o; - return Objects.equals(dbType, that.dbType) && Objects.equals(name, that.name) && Objects.equals(host, that - .host) && Objects.equals(port, that.port) && Objects.equals(schema, that.schema) && Objects.equals - (username, that.username) && Objects.equals(password, that.password) && Objects.equals(encoding, that - .encoding); + return Objects.equals(id, that.id) && + Objects.equals(dbType, that.dbType) && + Objects.equals(name, that.name) && + Objects.equals(host, that.host) && + Objects.equals(port, that.port) && + Objects.equals(schema, that.schema) && + Objects.equals(username, that.username) && + Objects.equals(password, that.password) && + Objects.equals(encoding, that.encoding) && + Objects.equals(lport, that.lport) && + Objects.equals(rport, that.rport) && + Objects.equals(sshPort, that.sshPort) && + Objects.equals(sshHost, that.sshHost) && + Objects.equals(sshUser, that.sshUser) && + Objects.equals(sshPassword, that.sshPassword); } @Override public int hashCode() { - return Objects.hash(dbType, name, host, port, schema, username, password, encoding); + return Objects.hash(id, dbType, name, host, port, schema, username, password, encoding, lport, rport, sshPort, sshHost, sshUser, sshPassword); } @Override public String toString() { - return "DatabaseConfig{" + "dbType='" + dbType + '\'' + ", name='" + name + '\'' + ", host='" + host + '\'' + - ", port='" + port + '\'' + ", schema='" + schema + '\'' + ", username='" + username + '\'' + ", " + - "password='" + password + '\'' + ", encoding='" + encoding + '\'' + '}'; + return "DatabaseConfig{" + + "id=" + id + + ", dbType='" + dbType + '\'' + + ", name='" + name + '\'' + + ", host='" + host + '\'' + + ", port='" + port + '\'' + + ", schema='" + schema + '\'' + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", encoding='" + encoding + '\'' + + ", lport='" + lport + '\'' + + ", rport='" + rport + '\'' + + ", sshPort='" + sshPort + '\'' + + ", sshHost='" + sshHost + '\'' + + ", sshUser='" + sshUser + '\'' + + ", sshPassword='" + sshPassword + '\'' + + '}'; } } diff --git a/src/main/java/com/zzg/mybatis/generator/util/ConfigHelper.java b/src/main/java/com/zzg/mybatis/generator/util/ConfigHelper.java index cd677739..aaaa680f 100644 --- a/src/main/java/com/zzg/mybatis/generator/util/ConfigHelper.java +++ b/src/main/java/com/zzg/mybatis/generator/util/ConfigHelper.java @@ -235,11 +235,11 @@ public static List getAllJDBCDriverJarPaths() { } else { file = new File("src/main/resources/lib"); } - System.out.println(file.getCanonicalPath()); + _LOG.info("jar lib path: {}", file.getCanonicalPath()); File[] jarFiles = file.listFiles(); - System.out.println("jarFiles:" + jarFiles); if (jarFiles != null && jarFiles.length > 0) { for (File jarFile : jarFiles) { + _LOG.info("jar file: {}", jarFile.getAbsolutePath()); if (jarFile.isFile() && jarFile.getAbsolutePath().endsWith(".jar")) { jarFilePathList.add(jarFile.getAbsolutePath()); } diff --git a/src/main/java/com/zzg/mybatis/generator/util/DbUtil.java b/src/main/java/com/zzg/mybatis/generator/util/DbUtil.java index 6763df84..e0216e16 100644 --- a/src/main/java/com/zzg/mybatis/generator/util/DbUtil.java +++ b/src/main/java/com/zzg/mybatis/generator/util/DbUtil.java @@ -1,15 +1,24 @@ package com.zzg.mybatis.generator.util; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; import com.zzg.mybatis.generator.exception.DbDriverLoadingException; import com.zzg.mybatis.generator.model.DatabaseConfig; import com.zzg.mybatis.generator.model.DbType; import com.zzg.mybatis.generator.model.UITableColumnVO; +import com.zzg.mybatis.generator.view.AlertUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.mybatis.generator.internal.util.ClassloaderUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by Owen on 6/12/16. @@ -21,6 +30,92 @@ public class DbUtil { private static Map drivers = new HashMap<>(); + private static ExecutorService executorService = Executors.newSingleThreadExecutor(); + private static volatile boolean portForwaring = false; + private static Map portForwardingSession = new ConcurrentHashMap<>(); + + public static Session getSSHSession(DatabaseConfig databaseConfig) { + if (StringUtils.isBlank(databaseConfig.getSshHost()) + || StringUtils.isBlank(databaseConfig.getSshPort()) + || StringUtils.isBlank(databaseConfig.getSshUser()) + || StringUtils.isBlank(databaseConfig.getSshPassword()) + ) { + return null; + } + Session session = null; + try { + //Set StrictHostKeyChecking property to no to avoid UnknownHostKey issue + java.util.Properties config = new java.util.Properties(); + config.put("StrictHostKeyChecking", "no"); + JSch jsch = new JSch(); + Integer sshPort = NumberUtils.createInteger(databaseConfig.getSshPort()); + int port = sshPort == null ? 22 : sshPort; + session = jsch.getSession(databaseConfig.getSshUser(), databaseConfig.getSshHost(), port); + session.setPassword(databaseConfig.getSshPassword()); + session.setConfig(config); + }catch (JSchException e) { + //Ignore + } + return session; + } + + public static void engagePortForwarding(Session sshSession, DatabaseConfig config) { + if (sshSession != null) { + AtomicInteger assinged_port = new AtomicInteger(); + Future result = executorService.submit(() -> { + try { + Integer localPort = NumberUtils.createInteger(config.getLport()); + Integer RemotePort = NumberUtils.createInteger(config.getRport()); + int lport = localPort == null ? Integer.parseInt(config.getPort()) : localPort; + int rport = RemotePort == null ? Integer.parseInt(config.getPort()) : RemotePort; + Session session = portForwardingSession.get(lport); + if (session != null && session.isConnected()) { + String s = session.getPortForwardingL()[0]; + String[] split = StringUtils.split(s, ":"); + boolean portForwarding = String.format("%s:%s", split[0], split[1]).equals(lport + ":" + config.getHost()); + if (portForwarding) { + return; + } + } + sshSession.connect(); + assinged_port.set(sshSession.setPortForwardingL(lport, config.getHost(), rport)); + portForwardingSession.put(lport, sshSession); + portForwaring = true; + _LOG.info("portForwarding Enabled, {}", assinged_port); + } catch (JSchException e) { + _LOG.error("Connect Over SSH failed", e); + if (e.getCause() != null && e.getCause().getMessage().equals("Address already in use: JVM_Bind")) { + throw new RuntimeException("Address already in use: JVM_Bind"); + } + throw new RuntimeException(e.getMessage()); + } + }); + try { + result.get(5, TimeUnit.SECONDS); + }catch (Exception e) { + shutdownPortForwarding(sshSession); + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException)e.getCause(); + } + if (e instanceof TimeoutException) { + throw new RuntimeException("OverSSH 连接超时:超过5秒"); + } + + _LOG.info("executorService isShutdown:{}", executorService.isShutdown()); + AlertUtil.showErrorAlert("OverSSH 失败,请检查连接设置:" + e.getMessage()); + } + } + } + + public static void shutdownPortForwarding(Session session) { + portForwaring = false; + if (session != null && session.isConnected()) { + session.disconnect(); + _LOG.info("portForwarding turn OFF"); + } +// executorService.shutdown(); + } + public static Connection getConnection(DatabaseConfig config) throws ClassNotFoundException, SQLException { DbType dbType = DbType.valueOf(config.getDbType()); if (drivers.get(dbType) == null){ @@ -40,9 +135,9 @@ public static Connection getConnection(DatabaseConfig config) throws ClassNotFou } public static List getTableNames(DatabaseConfig config) throws Exception { - String url = getConnectionUrlWithSchema(config); - _LOG.info("getTableNames, connection url: {}", url); - Connection connection = getConnection(config); + Session sshSession = getSSHSession(config); + engagePortForwarding(sshSession, config); + Connection connection = getConnection(config); try { List tables = new ArrayList<>(); DatabaseMetaData md = connection.getMetaData(); @@ -78,12 +173,15 @@ public static List getTableNames(DatabaseConfig config) throws Exception return tables; } finally { connection.close(); + shutdownPortForwarding(sshSession); } } public static List getTableColumns(DatabaseConfig dbConfig, String tableName) throws Exception { String url = getConnectionUrlWithSchema(dbConfig); _LOG.info("getTableColumns, connection url: {}", url); + Session sshSession = getSSHSession(dbConfig); + engagePortForwarding(sshSession, dbConfig); Connection conn = getConnection(dbConfig); try { DatabaseMetaData md = conn.getMetaData(); @@ -99,12 +197,14 @@ public static List getTableColumns(DatabaseConfig dbConfig, Str return columns; } finally { conn.close(); + shutdownPortForwarding(sshSession); } } public static String getConnectionUrlWithSchema(DatabaseConfig dbConfig) throws ClassNotFoundException { DbType dbType = DbType.valueOf(dbConfig.getDbType()); - String connectionUrl = String.format(dbType.getConnectionUrlPattern(), dbConfig.getHost(), dbConfig.getPort(), dbConfig.getSchema(), dbConfig.getEncoding()); + String connectionUrl = String.format(dbType.getConnectionUrlPattern(), + portForwaring ? "127.0.0.1" : dbConfig.getHost(), portForwaring ? dbConfig.getLport() : dbConfig.getPort(), dbConfig.getSchema(), dbConfig.getEncoding()); _LOG.info("getConnectionUrlWithSchema, connection url: {}", connectionUrl); return connectionUrl; } diff --git a/src/main/resources/fxml/basicConnection.fxml b/src/main/resources/fxml/basicConnection.fxml new file mode 100644 index 00000000..83f778f2 --- /dev/null +++ b/src/main/resources/fxml/basicConnection.fxml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/newConnection.fxml b/src/main/resources/fxml/newConnection.fxml index a44bfa4d..d65f6eeb 100644 --- a/src/main/resources/fxml/newConnection.fxml +++ b/src/main/resources/fxml/newConnection.fxml @@ -1,89 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/sshBasedConnection.fxml b/src/main/resources/fxml/sshBasedConnection.fxml new file mode 100644 index 00000000..b5057f40 --- /dev/null +++ b/src/main/resources/fxml/sshBasedConnection.fxml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/SSH_tunnel.png b/src/main/resources/icons/SSH_tunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ce3f6d298a55793c1bfb744205406f7705b5bf GIT binary patch literal 20758 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 KFBPd+2K@(;XiV|| literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/SSH_tunnel_disconnected.png b/src/main/resources/icons/SSH_tunnel_disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..1d3257c14b0def3dbc99325465eff1136e59639f GIT binary patch literal 21023 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 literal 0 HcmV?d00001