重构 MyBatis 生成输出布局与结果反馈
This commit is contained in:
+189
-40
@@ -2,6 +2,11 @@ package com.example.workutils.biz.mybatisgenerator;
|
||||
|
||||
import com.example.workutils.biz.mybatisgenerator.application.GenerateArtifactsRequest;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.GenerateArtifactsUseCase;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.GenerationPlan;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.GenerationPlanner;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.GenerationResult;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.OutputLayout;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.OutputLayoutPolicy;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.QueryTablesRequest;
|
||||
import com.example.workutils.biz.mybatisgenerator.application.QueryTablesUseCase;
|
||||
import com.example.workutils.biz.mybatisgenerator.presentation.DatasourceSummary;
|
||||
@@ -34,7 +39,6 @@ import javafx.stage.Stage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -57,8 +61,10 @@ public class MyBatisGeneratorWindow {
|
||||
private final WorkUtilsProperties properties;
|
||||
private final QueryTablesUseCase queryTablesUseCase;
|
||||
private final GenerateArtifactsUseCase generateArtifactsUseCase;
|
||||
private final OutputLayoutPolicy outputLayoutPolicy;
|
||||
private final GenerationPlanner generationPlanner;
|
||||
private final AsyncOperationController<List<String>> queryOperation;
|
||||
private final AsyncOperationController<String> generateOperation;
|
||||
private final AsyncOperationController<GenerationResult> generateOperation;
|
||||
|
||||
// ---- UI 组件 ----
|
||||
private TextField patternField;
|
||||
@@ -67,6 +73,8 @@ public class MyBatisGeneratorWindow {
|
||||
private TextField modelDirField;
|
||||
private TextField xmlDirField;
|
||||
private TextField mapperDirField;
|
||||
private TextField projectRootField;
|
||||
private CheckBox customOutputCb;
|
||||
private TreeView<String> projectTree;
|
||||
private ComboBox<String> datasourceCmb;
|
||||
private Label dbUrlLabel;
|
||||
@@ -115,9 +123,11 @@ public class MyBatisGeneratorWindow {
|
||||
this.generateArtifactsUseCase = new GenerateArtifactsUseCase(
|
||||
request -> callback.generate(
|
||||
request.tables(),
|
||||
request.modelDirectory(),
|
||||
request.xmlDirectory(),
|
||||
request.mapperDirectory()));
|
||||
request.outputLayout().modelDirectory().toString(),
|
||||
request.outputLayout().xmlDirectory().toString(),
|
||||
request.outputLayout().mapperDirectory().toString()));
|
||||
this.outputLayoutPolicy = new OutputLayoutPolicy();
|
||||
this.generationPlanner = new GenerationPlanner();
|
||||
this.queryOperation = new AsyncOperationController<>(
|
||||
AppExecutors.io(), Platform::runLater);
|
||||
this.generateOperation = new AsyncOperationController<>(
|
||||
@@ -429,7 +439,32 @@ public class MyBatisGeneratorWindow {
|
||||
Label dirLabel = new Label("输出目录设置:");
|
||||
dirLabel.setFont(Theme.heading(fsSection()));
|
||||
dirLabel.setTextFill(Theme.TEXT_PRIMARY);
|
||||
box.getChildren().add(dirLabel);
|
||||
projectRootField = new TextField();
|
||||
projectRootField.getStyleClass().add(Theme.CSS_TEXT_FIELD);
|
||||
HBox.setHgrow(projectRootField, Priority.ALWAYS);
|
||||
projectRootField.setOnAction(e -> refreshDerivedDirectories());
|
||||
projectRootField.focusedProperty().addListener((obs, oldValue, focused) -> {
|
||||
if (!focused) refreshDerivedDirectories();
|
||||
});
|
||||
|
||||
Button browseRootBtn = StyledButton.small(
|
||||
"浏览...", Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
browseRootBtn.setOnAction(e -> onBrowseProjectRoot());
|
||||
Button useTreeBtn = StyledButton.small(
|
||||
"使用选中项目", Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
useTreeBtn.setOnAction(e -> useSelectedProjectRoot());
|
||||
Label rootLabel = new Label("项目根目录:");
|
||||
rootLabel.setPrefWidth(110);
|
||||
HBox rootRow = new HBox(
|
||||
Theme.S2, rootLabel, projectRootField, browseRootBtn, useTreeBtn);
|
||||
rootRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
customOutputCb = new CheckBox("自定义输出目录(高级)");
|
||||
customOutputCb.setSelected(false);
|
||||
customOutputCb.selectedProperty().addListener((obs, oldValue, custom) -> {
|
||||
setDirectoryFieldsEditable(custom);
|
||||
if (!custom) refreshDerivedDirectories();
|
||||
});
|
||||
|
||||
HBox modelRow = createDirRow("Model (PO)", fm -> modelDirField = fm);
|
||||
modelDirField = (TextField) modelRow.getChildren().get(1);
|
||||
@@ -438,9 +473,12 @@ public class MyBatisGeneratorWindow {
|
||||
HBox mapperRow = createDirRow("Mapper 接口", fm -> mapperDirField = fm);
|
||||
mapperDirField = (TextField) mapperRow.getChildren().get(1);
|
||||
|
||||
loadDefaultDirs();
|
||||
initializeProjectRoot();
|
||||
setDirectoryFieldsEditable(false);
|
||||
|
||||
box.getChildren().addAll(modelRow, xmlRow, mapperRow);
|
||||
box.getChildren().addAll(
|
||||
dirLabel, rootRow, customOutputCb,
|
||||
modelRow, xmlRow, mapperRow);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -458,6 +496,7 @@ public class MyBatisGeneratorWindow {
|
||||
|
||||
Button browseBtn = StyledButton.small("浏览...", Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
browseBtn.setOnAction(e -> onBrowseDir(field));
|
||||
browseBtn.disableProperty().bind(field.editableProperty().not());
|
||||
|
||||
Button fillBtn = StyledButton.small("填充", Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
fillBtn.setOnAction(e -> {
|
||||
@@ -469,44 +508,85 @@ public class MyBatisGeneratorWindow {
|
||||
}
|
||||
}
|
||||
});
|
||||
fillBtn.disableProperty().bind(field.editableProperty().not());
|
||||
|
||||
if (fieldRef != null) fieldRef.accept(field);
|
||||
row.getChildren().addAll(lbl, field, browseBtn, fillBtn);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void loadDefaultDirs() {
|
||||
Properties p = new Properties();
|
||||
// 1. classpath 内置默认值
|
||||
try (InputStream is = getClass().getClassLoader().getResourceAsStream("mybatis-generator.properties")) {
|
||||
if (is != null) p.load(is);
|
||||
} catch (Exception ignored) {}
|
||||
// 2. data/ 外部配置覆盖
|
||||
Path dataFile = properties.getMybatisGeneratorFilePath();
|
||||
if (java.nio.file.Files.exists(dataFile)) {
|
||||
try (InputStream is = new java.io.FileInputStream(dataFile.toFile())) {
|
||||
Properties externalProps = new Properties();
|
||||
externalProps.load(is);
|
||||
p.putAll(externalProps);
|
||||
} catch (Exception ignored) {}
|
||||
private void initializeProjectRoot() {
|
||||
Path root = Paths.get("").toAbsolutePath().normalize();
|
||||
try {
|
||||
List<String> paths = fileOperationHelper.getProjectPaths();
|
||||
if (!paths.isEmpty() && Files.isDirectory(Paths.get(paths.get(0)))) {
|
||||
root = Paths.get(paths.get(0)).toAbsolutePath().normalize();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
projectRootField.setText(root.toString().replace('\\', '/'));
|
||||
refreshDerivedDirectories();
|
||||
}
|
||||
|
||||
setDirField(modelDirField, p.getProperty("model_target_project", "./generator"));
|
||||
setDirField(xmlDirField, p.getProperty("sqlmapper_target_project", "./generator"));
|
||||
setDirField(mapperDirField, p.getProperty("dao_target_project", "./generator"));
|
||||
private void setDirectoryFieldsEditable(boolean editable) {
|
||||
modelDirField.setEditable(editable);
|
||||
xmlDirField.setEditable(editable);
|
||||
mapperDirField.setEditable(editable);
|
||||
}
|
||||
|
||||
private void setDirField(TextField field, String dir) {
|
||||
if (field == null || dir == null) return;
|
||||
if (dir.startsWith("./") || dir.startsWith(".\\")) {
|
||||
field.setText(new File(dir).getAbsolutePath().replace('\\', '/'));
|
||||
} else {
|
||||
field.setText(dir.replace('\\', '/'));
|
||||
private void refreshDerivedDirectories() {
|
||||
if (customOutputCb != null && customOutputCb.isSelected()) return;
|
||||
try {
|
||||
OutputLayout layout = outputLayoutPolicy.derive(
|
||||
Paths.get(projectRootField.getText().trim()));
|
||||
setDirectoryFields(layout);
|
||||
} catch (IllegalArgumentException error) {
|
||||
modelDirField.clear();
|
||||
xmlDirField.clear();
|
||||
mapperDirField.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDirectoryFields(OutputLayout layout) {
|
||||
modelDirField.setText(pathText(layout.modelDirectory()));
|
||||
xmlDirField.setText(pathText(layout.xmlDirectory()));
|
||||
mapperDirField.setText(pathText(layout.mapperDirectory()));
|
||||
}
|
||||
|
||||
private String pathText(Path path) {
|
||||
return path.toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
// ==================== 事件 ====================
|
||||
|
||||
private void onBrowseProjectRoot() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle("选择项目根目录");
|
||||
Path current = parseProjectRoot();
|
||||
if (current != null && Files.isDirectory(current)) {
|
||||
chooser.setInitialDirectory(current.toFile());
|
||||
}
|
||||
File selected = chooser.showDialog(stage);
|
||||
if (selected != null) {
|
||||
projectRootField.setText(pathText(selected.toPath()));
|
||||
refreshDerivedDirectories();
|
||||
}
|
||||
}
|
||||
|
||||
private void useSelectedProjectRoot() {
|
||||
TreeItem<String> selected = projectTree.getSelectionModel().getSelectedItem();
|
||||
String path = buildTreePath(selected);
|
||||
if (path != null && Files.isDirectory(Paths.get(path))) {
|
||||
projectRootField.setText(pathText(Paths.get(path)));
|
||||
refreshDerivedDirectories();
|
||||
}
|
||||
}
|
||||
|
||||
private Path parseProjectRoot() {
|
||||
String text = projectRootField == null ? "" : projectRootField.getText().trim();
|
||||
return text.isEmpty() ? null : Paths.get(text).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private void onBrowseDir(TextField targetField) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle("选择目录");
|
||||
@@ -565,28 +645,97 @@ public class MyBatisGeneratorWindow {
|
||||
|
||||
private void onGenerate() {
|
||||
try {
|
||||
GenerateArtifactsRequest request = new GenerateArtifactsRequest(
|
||||
tableItems.stream()
|
||||
List<String> selectedTables = tableItems.stream()
|
||||
.filter(TableItem::isSelected)
|
||||
.map(TableItem::getTableName)
|
||||
.collect(Collectors.toList()),
|
||||
getDirText(modelDirField),
|
||||
getDirText(xmlDirField),
|
||||
getDirText(mapperDirField));
|
||||
.collect(Collectors.toList());
|
||||
OutputLayout layout = buildOutputLayout();
|
||||
GenerateArtifactsRequest request = new GenerateArtifactsRequest(
|
||||
selectedTables, layout);
|
||||
GenerationPlan plan = generationPlanner.plan(selectedTables, layout);
|
||||
if (!confirmGeneration(plan)) return;
|
||||
generateOperation.start(
|
||||
"正在生成代码",
|
||||
context -> generateArtifactsUseCase.execute(request),
|
||||
result -> showInfo("生成结果", result),
|
||||
result -> showInfo("生成结果", formatGenerationResult(result)),
|
||||
error -> showAlert("生成失败: " + error.getMessage()));
|
||||
} catch (IllegalArgumentException error) {
|
||||
} catch (RuntimeException error) {
|
||||
showAlert(error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private OutputLayout buildOutputLayout() {
|
||||
Path root = parseProjectRoot();
|
||||
if (root == null) {
|
||||
throw new IllegalArgumentException("请选择项目根目录");
|
||||
}
|
||||
if (!customOutputCb.isSelected()) {
|
||||
return outputLayoutPolicy.derive(root);
|
||||
}
|
||||
return outputLayoutPolicy.custom(
|
||||
root,
|
||||
Paths.get(getDirText(modelDirField)),
|
||||
Paths.get(getDirText(xmlDirField)),
|
||||
Paths.get(getDirText(mapperDirField)));
|
||||
}
|
||||
|
||||
private boolean confirmGeneration(GenerationPlan plan) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("确认生成");
|
||||
alert.setHeaderText("即将为 " + plan.tables().size() + " 个表生成代码");
|
||||
alert.setContentText(buildGenerationSummary(plan));
|
||||
alert.initOwner(stage);
|
||||
return alert.showAndWait().filter(ButtonType.OK::equals).isPresent();
|
||||
}
|
||||
|
||||
private String buildGenerationSummary(GenerationPlan plan) {
|
||||
return "表: " + String.join(", ", plan.tables())
|
||||
+ "\n\nModel: " + formatRisk(plan.modelRisk())
|
||||
+ "\nXML Mapper: " + formatRisk(plan.xmlRisk())
|
||||
+ "\nMapper 接口: " + formatRisk(plan.mapperRisk())
|
||||
+ "\n\n已有文件可能被生成器覆盖,请确认后继续。";
|
||||
}
|
||||
|
||||
private String formatRisk(
|
||||
com.example.workutils.biz.mybatisgenerator.application.DirectoryRisk risk) {
|
||||
String state = risk.willCreate()
|
||||
? "待创建"
|
||||
: "已有 " + risk.existingFileCount() + " 个相关文件";
|
||||
return pathText(risk.directory()) + "(" + state + ")";
|
||||
}
|
||||
|
||||
private String formatGenerationResult(GenerationResult result) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
text.append("新增文件: ").append(result.createdFiles().size()).append('\n');
|
||||
appendFiles(text, result.createdFiles());
|
||||
text.append("\n更新文件: ").append(result.updatedFiles().size()).append('\n');
|
||||
appendFiles(text, result.updatedFiles());
|
||||
text.append("\n未变化文件: ").append(result.unchangedFiles().size()).append('\n');
|
||||
if (!result.warnings().isEmpty()) {
|
||||
text.append("\n警告:\n");
|
||||
result.warnings().forEach(warning -> text.append("- ").append(warning).append('\n'));
|
||||
}
|
||||
if (!result.generatorMessage().isBlank()) {
|
||||
text.append("\n生成器消息:\n").append(result.generatorMessage());
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
private void appendFiles(StringBuilder text, List<Path> files) {
|
||||
int visibleCount = Math.min(files.size(), 20);
|
||||
for (int index = 0; index < visibleCount; index++) {
|
||||
text.append("- ").append(pathText(files.get(index))).append('\n');
|
||||
}
|
||||
if (files.size() > visibleCount) {
|
||||
text.append("... 另有 ").append(files.size() - visibleCount).append(" 个文件\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void resetForm() {
|
||||
patternField.clear();
|
||||
tableItems.clear();
|
||||
loadDefaultDirs();
|
||||
customOutputCb.setSelected(false);
|
||||
initializeProjectRoot();
|
||||
}
|
||||
|
||||
private String getDirText(TextField field) {
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record DirectoryRisk(
|
||||
Path directory,
|
||||
boolean exists,
|
||||
boolean writable,
|
||||
boolean willCreate,
|
||||
long existingFileCount) {
|
||||
|
||||
public DirectoryRisk {
|
||||
directory = Objects.requireNonNull(directory, "directory")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (existingFileCount < 0) {
|
||||
throw new IllegalArgumentException("existingFileCount 不能为负数");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-12
@@ -5,23 +5,13 @@ import java.util.Objects;
|
||||
|
||||
public record GenerateArtifactsRequest(
|
||||
List<String> tables,
|
||||
String modelDirectory,
|
||||
String xmlDirectory,
|
||||
String mapperDirectory) {
|
||||
OutputLayout outputLayout) {
|
||||
|
||||
public GenerateArtifactsRequest {
|
||||
tables = List.copyOf(Objects.requireNonNull(tables, "tables"));
|
||||
if (tables.isEmpty()) {
|
||||
throw new IllegalArgumentException("请至少选择一个表");
|
||||
}
|
||||
modelDirectory = directoryOrDefault(modelDirectory);
|
||||
xmlDirectory = directoryOrDefault(xmlDirectory);
|
||||
mapperDirectory = directoryOrDefault(mapperDirectory);
|
||||
}
|
||||
|
||||
private static String directoryOrDefault(String value) {
|
||||
return value == null || value.isBlank()
|
||||
? "./generator"
|
||||
: value.trim();
|
||||
outputLayout = Objects.requireNonNull(outputLayout, "outputLayout");
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -10,12 +10,26 @@ public final class GenerateArtifactsUseCase {
|
||||
}
|
||||
|
||||
private final Gateway gateway;
|
||||
private final GenerationPlanner planner;
|
||||
|
||||
public GenerateArtifactsUseCase(Gateway gateway) {
|
||||
this.gateway = Objects.requireNonNull(gateway, "gateway");
|
||||
this(gateway, new GenerationPlanner());
|
||||
}
|
||||
|
||||
public String execute(GenerateArtifactsRequest request) throws Exception {
|
||||
return gateway.generate(Objects.requireNonNull(request, "request"));
|
||||
GenerateArtifactsUseCase(
|
||||
Gateway gateway,
|
||||
GenerationPlanner planner) {
|
||||
this.gateway = Objects.requireNonNull(gateway, "gateway");
|
||||
this.planner = Objects.requireNonNull(planner, "planner");
|
||||
}
|
||||
|
||||
public GenerationResult execute(GenerateArtifactsRequest request) throws Exception {
|
||||
GenerateArtifactsRequest safeRequest =
|
||||
Objects.requireNonNull(request, "request");
|
||||
planner.plan(safeRequest.tables(), safeRequest.outputLayout());
|
||||
OutputSnapshot before = OutputSnapshot.capture(safeRequest.outputLayout());
|
||||
String message = gateway.generate(safeRequest);
|
||||
OutputSnapshot after = OutputSnapshot.capture(safeRequest.outputLayout());
|
||||
return before.compare(after, message);
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record GenerationPlan(
|
||||
List<String> tables,
|
||||
DirectoryRisk modelRisk,
|
||||
DirectoryRisk xmlRisk,
|
||||
DirectoryRisk mapperRisk) {
|
||||
|
||||
public GenerationPlan {
|
||||
tables = List.copyOf(Objects.requireNonNull(tables, "tables"));
|
||||
modelRisk = Objects.requireNonNull(modelRisk, "modelRisk");
|
||||
xmlRisk = Objects.requireNonNull(xmlRisk, "xmlRisk");
|
||||
mapperRisk = Objects.requireNonNull(mapperRisk, "mapperRisk");
|
||||
}
|
||||
|
||||
public void validateWritableDirectories() {
|
||||
validate(modelRisk);
|
||||
validate(xmlRisk);
|
||||
validate(mapperRisk);
|
||||
}
|
||||
|
||||
private void validate(DirectoryRisk risk) {
|
||||
if (risk.exists() && !risk.writable()) {
|
||||
throw new IllegalArgumentException("输出目录不可写: " + risk.directory());
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class GenerationPlanner {
|
||||
|
||||
public GenerationPlan plan(List<String> tables, OutputLayout layout) {
|
||||
Objects.requireNonNull(layout, "layout");
|
||||
GenerationPlan plan = new GenerationPlan(
|
||||
List.copyOf(Objects.requireNonNull(tables, "tables")),
|
||||
inspect(layout.modelDirectory(), ".java"),
|
||||
inspect(layout.xmlDirectory(), ".xml"),
|
||||
inspect(layout.mapperDirectory(), ".java"));
|
||||
plan.validateWritableDirectories();
|
||||
return plan;
|
||||
}
|
||||
|
||||
private DirectoryRisk inspect(Path directory, String extension) {
|
||||
boolean exists = Files.isDirectory(directory);
|
||||
return new DirectoryRisk(
|
||||
directory,
|
||||
exists,
|
||||
!exists || Files.isWritable(directory),
|
||||
!exists,
|
||||
exists ? countFiles(directory, extension) : 0);
|
||||
}
|
||||
|
||||
private long countFiles(Path directory, String extension) {
|
||||
try (Stream<Path> paths = Files.walk(directory)) {
|
||||
String suffix = extension.toLowerCase(Locale.ROOT);
|
||||
return paths.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString()
|
||||
.toLowerCase(Locale.ROOT).endsWith(suffix))
|
||||
.count();
|
||||
} catch (IOException error) {
|
||||
throw new UncheckedIOException(
|
||||
"无法检查输出目录: " + directory, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record GenerationResult(
|
||||
List<Path> createdFiles,
|
||||
List<Path> updatedFiles,
|
||||
List<Path> unchangedFiles,
|
||||
List<String> warnings,
|
||||
String generatorMessage) {
|
||||
|
||||
public GenerationResult {
|
||||
createdFiles = List.copyOf(Objects.requireNonNull(createdFiles, "createdFiles"));
|
||||
updatedFiles = List.copyOf(Objects.requireNonNull(updatedFiles, "updatedFiles"));
|
||||
unchangedFiles = List.copyOf(Objects.requireNonNull(unchangedFiles, "unchangedFiles"));
|
||||
warnings = List.copyOf(Objects.requireNonNull(warnings, "warnings"));
|
||||
generatorMessage = generatorMessage == null ? "" : generatorMessage;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record OutputLayout(
|
||||
Path projectRoot,
|
||||
Path modelDirectory,
|
||||
Path xmlDirectory,
|
||||
Path mapperDirectory) {
|
||||
|
||||
public OutputLayout {
|
||||
projectRoot = normalize(projectRoot, "projectRoot");
|
||||
modelDirectory = normalize(modelDirectory, "modelDirectory");
|
||||
xmlDirectory = normalize(xmlDirectory, "xmlDirectory");
|
||||
mapperDirectory = normalize(mapperDirectory, "mapperDirectory");
|
||||
}
|
||||
|
||||
private static Path normalize(Path path, String name) {
|
||||
return Objects.requireNonNull(path, name)
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class OutputLayoutPolicy {
|
||||
|
||||
public OutputLayout derive(Path projectRoot) {
|
||||
Path root = requireProjectRoot(projectRoot);
|
||||
return new OutputLayout(
|
||||
root,
|
||||
root.resolve("src/main/java"),
|
||||
root.resolve("src/main/resources"),
|
||||
root.resolve("src/main/java"));
|
||||
}
|
||||
|
||||
public OutputLayout custom(
|
||||
Path projectRoot,
|
||||
Path modelDirectory,
|
||||
Path xmlDirectory,
|
||||
Path mapperDirectory) {
|
||||
return new OutputLayout(
|
||||
requireProjectRoot(projectRoot),
|
||||
Objects.requireNonNull(modelDirectory, "modelDirectory"),
|
||||
Objects.requireNonNull(xmlDirectory, "xmlDirectory"),
|
||||
Objects.requireNonNull(mapperDirectory, "mapperDirectory"));
|
||||
}
|
||||
|
||||
private Path requireProjectRoot(Path projectRoot) {
|
||||
Path root = Objects.requireNonNull(projectRoot, "projectRoot")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!Files.isDirectory(root)) {
|
||||
throw new IllegalArgumentException("项目根目录不存在或不是目录: " + root);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class OutputSnapshot {
|
||||
|
||||
private final Map<Path, FileFingerprint> files;
|
||||
private final List<String> warnings;
|
||||
|
||||
private OutputSnapshot(
|
||||
Map<Path, FileFingerprint> files,
|
||||
List<String> warnings) {
|
||||
this.files = Map.copyOf(files);
|
||||
this.warnings = List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public static OutputSnapshot capture(OutputLayout layout) {
|
||||
Map<Path, FileFingerprint> files = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
Set<Path> roots = new LinkedHashSet<>(List.of(
|
||||
layout.modelDirectory(),
|
||||
layout.xmlDirectory(),
|
||||
layout.mapperDirectory()));
|
||||
for (Path root : roots) {
|
||||
captureRoot(root, files, warnings);
|
||||
}
|
||||
return new OutputSnapshot(files, warnings);
|
||||
}
|
||||
|
||||
public GenerationResult compare(
|
||||
OutputSnapshot after,
|
||||
String generatorMessage) {
|
||||
List<Path> created = new ArrayList<>();
|
||||
List<Path> updated = new ArrayList<>();
|
||||
List<Path> unchanged = new ArrayList<>();
|
||||
for (Map.Entry<Path, FileFingerprint> entry : after.files.entrySet()) {
|
||||
FileFingerprint previous = files.get(entry.getKey());
|
||||
if (previous == null) {
|
||||
created.add(entry.getKey());
|
||||
} else if (!previous.equals(entry.getValue())) {
|
||||
updated.add(entry.getKey());
|
||||
} else {
|
||||
unchanged.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
created.sort(Path::compareTo);
|
||||
updated.sort(Path::compareTo);
|
||||
unchanged.sort(Path::compareTo);
|
||||
List<String> combinedWarnings = new ArrayList<>(warnings);
|
||||
combinedWarnings.addAll(after.warnings);
|
||||
return new GenerationResult(
|
||||
created, updated, unchanged,
|
||||
combinedWarnings, generatorMessage);
|
||||
}
|
||||
|
||||
private static void captureRoot(
|
||||
Path root,
|
||||
Map<Path, FileFingerprint> files,
|
||||
List<String> warnings) {
|
||||
if (!Files.isDirectory(root)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(root)) {
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(OutputSnapshot::isGeneratedSource)
|
||||
.forEach(path -> captureFile(path, files, warnings));
|
||||
} catch (IOException error) {
|
||||
warnings.add("无法扫描输出目录 " + root + ": " + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void captureFile(
|
||||
Path path,
|
||||
Map<Path, FileFingerprint> files,
|
||||
List<String> warnings) {
|
||||
Path normalized = path.toAbsolutePath().normalize();
|
||||
try {
|
||||
files.put(normalized, new FileFingerprint(
|
||||
Files.size(normalized),
|
||||
Files.getLastModifiedTime(normalized).toMillis()));
|
||||
} catch (IOException error) {
|
||||
warnings.add("无法读取输出文件 " + normalized + ": " + error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGeneratedSource(Path path) {
|
||||
String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
|
||||
return name.endsWith(".java") || name.endsWith(".xml");
|
||||
}
|
||||
|
||||
private record FileFingerprint(long size, long modifiedMillis) {
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.example.workutils.biz.mybatisgenerator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MyBatisGeneratorWindowArchitectureTest {
|
||||
|
||||
@Test
|
||||
void usesProjectRootPolicyAndAdvancedCustomDirectoryMode() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java"));
|
||||
|
||||
assertTrue(source.contains("projectRootField"));
|
||||
assertTrue(source.contains("customOutputCb"));
|
||||
assertTrue(source.contains("OutputLayoutPolicy"));
|
||||
assertTrue(source.contains("setDirectoryFieldsEditable"));
|
||||
assertTrue(source.contains("buildGenerationSummary"));
|
||||
assertTrue(source.contains("formatGenerationResult"));
|
||||
assertFalse(source.contains("Files.walk"));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GenerationPlannerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void reportsExistingFilesAndDirectoriesWaitingToBeCreated() throws IOException {
|
||||
Path javaRoot = Files.createDirectories(tempDir.resolve("src/main/java"));
|
||||
Files.writeString(javaRoot.resolve("Account.java"), "class Account {}");
|
||||
Files.writeString(javaRoot.resolve("README.txt"), "ignored");
|
||||
OutputLayout layout = new OutputLayout(
|
||||
tempDir,
|
||||
javaRoot,
|
||||
tempDir.resolve("src/main/resources"),
|
||||
javaRoot);
|
||||
|
||||
GenerationPlan plan = new GenerationPlanner().plan(
|
||||
List.of("account", "trade"), layout);
|
||||
|
||||
assertEquals(List.of("account", "trade"), plan.tables());
|
||||
assertEquals(1, plan.modelRisk().existingFileCount());
|
||||
assertEquals(1, plan.mapperRisk().existingFileCount());
|
||||
assertFalse(plan.xmlRisk().exists());
|
||||
assertTrue(plan.xmlRisk().willCreate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAPlanContainingAnUnwritableExistingDirectory() {
|
||||
DirectoryRisk blocked = new DirectoryRisk(
|
||||
tempDir.resolve("blocked"), true, false, false, 0);
|
||||
GenerationPlan plan = new GenerationPlan(
|
||||
List.of("account"), blocked, blocked, blocked);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
plan::validateWritableDirectories);
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -1,7 +1,9 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,6 +12,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class GeneratorRequestTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void queryRequiresAndTrimsPattern() {
|
||||
assertEquals("trade%",
|
||||
@@ -19,23 +24,22 @@ class GeneratorRequestTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateCopiesTablesAndDefaultsDirectories() {
|
||||
void generateCopiesTablesAndKeepsOutputLayout() {
|
||||
List<String> tables = new ArrayList<>(List.of("trade_order"));
|
||||
OutputLayout layout = new OutputLayoutPolicy().derive(tempDir);
|
||||
GenerateArtifactsRequest request = new GenerateArtifactsRequest(
|
||||
tables, " ", null, "./custom");
|
||||
tables, layout);
|
||||
|
||||
tables.add("trade_item");
|
||||
|
||||
assertEquals(List.of("trade_order"), request.tables());
|
||||
assertEquals("./generator", request.modelDirectory());
|
||||
assertEquals("./generator", request.xmlDirectory());
|
||||
assertEquals("./custom", request.mapperDirectory());
|
||||
assertEquals(layout, request.outputLayout());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateRequiresSelectedTable() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new GenerateArtifactsRequest(
|
||||
List.of(), "a", "b", "c"));
|
||||
List.of(), new OutputLayoutPolicy().derive(tempDir)));
|
||||
}
|
||||
}
|
||||
|
||||
+17
-4
@@ -1,7 +1,10 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -10,6 +13,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class GeneratorUseCaseTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void queryDelegatesAndCopiesResult() throws Exception {
|
||||
List<String> gatewayResult =
|
||||
@@ -25,20 +31,27 @@ class GeneratorUseCaseTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateDelegatesCompleteRequest() throws Exception {
|
||||
void generateDelegatesRequestAndReturnsFileDifference() throws Exception {
|
||||
AtomicReference<GenerateArtifactsRequest> received =
|
||||
new AtomicReference<>();
|
||||
OutputLayout layout = new OutputLayoutPolicy().derive(tempDir);
|
||||
GenerateArtifactsUseCase useCase =
|
||||
new GenerateArtifactsUseCase(request -> {
|
||||
received.set(request);
|
||||
Files.createDirectories(request.outputLayout().modelDirectory());
|
||||
Files.writeString(
|
||||
request.outputLayout().modelDirectory().resolve("TradeOrder.java"),
|
||||
"class TradeOrder {}");
|
||||
return "generated";
|
||||
});
|
||||
GenerateArtifactsRequest request = new GenerateArtifactsRequest(
|
||||
List.of("trade_order"), "model", "xml", "mapper");
|
||||
List.of("trade_order"), layout);
|
||||
|
||||
String result = useCase.execute(request);
|
||||
GenerationResult result = useCase.execute(request);
|
||||
|
||||
assertEquals("generated", result);
|
||||
assertEquals("generated", result.generatorMessage());
|
||||
assertEquals(List.of(layout.modelDirectory().resolve("TradeOrder.java")),
|
||||
result.createdFiles());
|
||||
assertEquals(request, received.get());
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class OutputLayoutPolicyTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void derivesStandardDirectoriesFromProjectRoot() {
|
||||
OutputLayout layout = new OutputLayoutPolicy().derive(tempDir);
|
||||
|
||||
Path root = tempDir.toAbsolutePath().normalize();
|
||||
assertEquals(root, layout.projectRoot());
|
||||
assertEquals(root.resolve("src/main/java"), layout.modelDirectory());
|
||||
assertEquals(root.resolve("src/main/resources"), layout.xmlDirectory());
|
||||
assertEquals(root.resolve("src/main/java"), layout.mapperDirectory());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsARegularFileAsProjectRoot() throws IOException {
|
||||
Path file = Files.createFile(tempDir.resolve("pom.xml"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new OutputLayoutPolicy().derive(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsExplicitCustomDirectories() {
|
||||
OutputLayout layout = new OutputLayoutPolicy().custom(
|
||||
tempDir,
|
||||
tempDir.resolve("model-output"),
|
||||
tempDir.resolve("xml-output"),
|
||||
tempDir.resolve("mapper-output"));
|
||||
|
||||
assertEquals(tempDir.resolve("model-output").toAbsolutePath().normalize(),
|
||||
layout.modelDirectory());
|
||||
assertEquals(tempDir.resolve("xml-output").toAbsolutePath().normalize(),
|
||||
layout.xmlDirectory());
|
||||
assertEquals(tempDir.resolve("mapper-output").toAbsolutePath().normalize(),
|
||||
layout.mapperDirectory());
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.example.workutils.biz.mybatisgenerator.application;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class OutputSnapshotTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void classifiesCreatedUpdatedAndUnchangedFiles() throws IOException {
|
||||
Path javaRoot = Files.createDirectories(tempDir.resolve("src/main/java"));
|
||||
Path xmlRoot = Files.createDirectories(tempDir.resolve("src/main/resources"));
|
||||
Path updated = Files.writeString(javaRoot.resolve("Account.java"), "before");
|
||||
Path unchanged = Files.writeString(xmlRoot.resolve("Account.xml"), "same");
|
||||
OutputLayout layout = new OutputLayout(tempDir, javaRoot, xmlRoot, javaRoot);
|
||||
OutputSnapshot before = OutputSnapshot.capture(layout);
|
||||
|
||||
Files.writeString(updated, "after-content");
|
||||
Files.setLastModifiedTime(updated, FileTime.fromMillis(
|
||||
Files.getLastModifiedTime(updated).toMillis() + 2_000));
|
||||
Path created = Files.writeString(javaRoot.resolve("AccountMapper.java"), "new");
|
||||
OutputSnapshot after = OutputSnapshot.capture(layout);
|
||||
|
||||
GenerationResult result = before.compare(after, "generated");
|
||||
|
||||
assertEquals(java.util.List.of(created.toAbsolutePath().normalize()),
|
||||
result.createdFiles());
|
||||
assertEquals(java.util.List.of(updated.toAbsolutePath().normalize()),
|
||||
result.updatedFiles());
|
||||
assertEquals(java.util.List.of(unchanged.toAbsolutePath().normalize()),
|
||||
result.unchangedFiles());
|
||||
assertEquals("generated", result.generatorMessage());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user