重构 MyBatis 生成输出布局与结果反馈

This commit is contained in:
张家训
2026-07-31 18:45:55 +08:00
parent dd6063ebdc
commit ac905763b9
16 changed files with 696 additions and 67 deletions
@@ -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) {
@@ -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 不能为负数");
}
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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());
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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) {
}
}
@@ -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"));
}
}
@@ -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);
}
}
@@ -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)));
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}