diff --git a/build.gradle b/build.gradle
index 9f0a5b0..e33d3c0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ plugins {
}
group 'com.dmiki'
-version '1.0.0'
+version '1.1.0'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 8e9202b..2c03e38 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -7,7 +7,7 @@ MetaPlugin 是一个 IntelliJ IDEA 插件,用于把 XSD 结构转换为可配
当前项目中的插件信息:
- 插件名:`MetaPlugin`
- 插件 ID:`com.dmiki.metaplugin`
-- 当前版本:`1.0.0`
+- 当前版本:`1.1.0`
- IDE 兼容下限:`sinceBuild = 193`(IntelliJ IDEA 2019.3+)
## 2. 核心能力
@@ -46,7 +46,7 @@ MetaPlugin 是一个 IntelliJ IDEA 插件,用于把 XSD 结构转换为可配
打包产物位于:
-- `build/distributions/MetaPlugin-1.0.0.zip`
+- `build/distributions/MetaPlugin-1.1.0.zip`
在 IDEA 中通过 `Settings -> Plugins -> Install Plugin from Disk...` 选择该 ZIP 安装。
@@ -93,7 +93,7 @@ MetaPlugin 是一个 IntelliJ IDEA 插件,用于把 XSD 结构转换为可配
- 包名:默认 `com.example.message`。
- 注意这里填写的是“包名”,最终 `resultMap.type` 会自动拼接为:`包名 + 报文类型大写格式`。
- 例如 `hvps.101.001.01` 会转为 `HVPS_101_001_01`。
-- 输出目录:支持绝对路径或相对项目根目录路径。
+- 输出目录:支持绝对路径或相对模块根目录路径。
- 自定义属性:全局默认值,默认 `sign`。
- 预处理属性:全局默认值,默认空。
@@ -147,11 +147,9 @@ MetaPlugin 是一个 IntelliJ IDEA 插件,用于把 XSD 结构转换为可配
### 8.3 输出目录与文件名
-- 默认输出目录:`src/main/resources` 下第一个不存在的目录:
- - `msgmapper`
- - `msgmapper1`
- - `msgmapper2`
- - ... 依次类推
+- 默认输出目录分两种情况:
+ - 如果 XSD 位于某个 `resources` 目录下,则输出到该 `resources` 目录下第一个不存在的 `msgmapperN`
+ - 否则输出到 `XSD 父目录` 的同级第一个不存在的 `msgmapperN`
- 默认输出文件名:`报文类型.xml`。
- 右键“生成选中节点XOM文件”时,默认文件名:`报文类型_节点名.xml`。
diff --git a/docs/hvps.101.001.01.xsd b/docs/hvps.101.001.01.xsd
new file mode 100644
index 0000000..ee792c1
--- /dev/null
+++ b/docs/hvps.101.001.01.xsd
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/dmiki/metaplugin/xom/XomGenerationService.java b/src/main/java/com/dmiki/metaplugin/xom/XomGenerationService.java
index b31122c..90c6d64 100644
--- a/src/main/java/com/dmiki/metaplugin/xom/XomGenerationService.java
+++ b/src/main/java/com/dmiki/metaplugin/xom/XomGenerationService.java
@@ -65,16 +65,18 @@ public class XomGenerationService {
private String resolveOutputPath(XomGenerateOptions options) throws XomGenerationException {
String outputPath = trimToNull(options.getOutputPath());
+ File xsdFile = resolveXsdFile(options);
+ File moduleBaseDir = XomPathStrategy.resolveModuleBaseDir(options.getProjectBasePath(), xsdFile);
if (outputPath != null) {
- File outputFile = resolveOutputFile(outputPath, options);
+ File outputFile = resolveOutputFile(outputPath, moduleBaseDir);
if (outputFile.isDirectory()) {
return new File(outputFile, safeName(options.getSchemaId()) + ".xml").getAbsolutePath();
}
return outputFile.getAbsolutePath();
}
- File resourcesDir = resolveResourcesDirectory(options);
- File targetDirectory = resolveNextMsgMapperDirectory(resourcesDir);
+ File targetDirectory = XomPathStrategy.resolveNextMsgMapperDirectory(
+ XomPathStrategy.resolveDefaultOutputBaseDir(xsdFile));
String schemaId = trimToNull(options.getSchemaId());
if (schemaId == null) {
schemaId = nameFromXsd(options);
@@ -82,46 +84,16 @@ public class XomGenerationService {
return new File(targetDirectory, safeName(schemaId) + ".xml").getAbsolutePath();
}
- private File resolveOutputFile(String outputPath, XomGenerateOptions options) {
- File outputFile = new File(outputPath);
- if (outputFile.isAbsolute()) {
- return outputFile;
- }
- String projectBasePath = trimToNull(options.getProjectBasePath());
- if (projectBasePath == null) {
- return outputFile;
- }
- return new File(projectBasePath, outputPath);
+ private File resolveOutputFile(String outputPath, File moduleBaseDir) {
+ return XomPathStrategy.resolvePath(outputPath, moduleBaseDir);
}
- private File resolveResourcesDirectory(XomGenerateOptions options) throws XomGenerationException {
- String projectBasePath = trimToNull(options.getProjectBasePath());
- if (projectBasePath != null) {
- return new File(projectBasePath, "src/main/resources");
- }
-
+ private File resolveXsdFile(XomGenerateOptions options) throws XomGenerationException {
String xsdPath = trimToNull(options.getXsdPath());
if (xsdPath == null) {
throw new XomGenerationException("XOM-001", "无法推导输出路径,xsdPath为空");
}
- File xsdFile = new File(xsdPath);
- File parent = xsdFile.getParentFile();
- if (parent == null) {
- throw new XomGenerationException("XOM-001", "无法推导输出路径,XSD父目录为空");
- }
- return parent;
- }
-
- private File resolveNextMsgMapperDirectory(File resourcesDir) {
- int suffix = 0;
- while (true) {
- String name = suffix == 0 ? "msgmapper" : "msgmapper" + suffix;
- File candidate = new File(resourcesDir, name);
- if (!candidate.exists()) {
- return candidate;
- }
- suffix++;
- }
+ return new File(xsdPath);
}
private String nameFromXsd(XomGenerateOptions options) {
diff --git a/src/main/java/com/dmiki/metaplugin/xom/XomPathStrategy.java b/src/main/java/com/dmiki/metaplugin/xom/XomPathStrategy.java
new file mode 100644
index 0000000..8b9b215
--- /dev/null
+++ b/src/main/java/com/dmiki/metaplugin/xom/XomPathStrategy.java
@@ -0,0 +1,180 @@
+package com.dmiki.metaplugin.xom;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Locale;
+
+public final class XomPathStrategy {
+ private static final String[] MODULE_MARKERS = {
+ "build.gradle",
+ "build.gradle.kts",
+ "pom.xml",
+ ".idea"
+ };
+
+ private XomPathStrategy() {
+ }
+
+ public static File resolveModuleBaseDir(String basePath, File anchorFile) {
+ String normalizedBasePath = trimToNull(basePath);
+ if (normalizedBasePath != null) {
+ return new File(normalizedBasePath);
+ }
+ return inferModuleBaseDir(anchorFile);
+ }
+
+ public static File inferModuleBaseDir(File anchorFile) {
+ File current = toDirectory(anchorFile);
+ while (current != null) {
+ if (containsModuleMarker(current)) {
+ return current;
+ }
+ current = current.getParentFile();
+ }
+
+ File srcDir = findAncestorByName(anchorFile, "src");
+ if (srcDir != null && srcDir.getParentFile() != null) {
+ return srcDir.getParentFile();
+ }
+
+ File anchorDir = toDirectory(anchorFile);
+ if (anchorDir == null) {
+ return null;
+ }
+ File parent = anchorDir.getParentFile();
+ return parent == null ? anchorDir : parent;
+ }
+
+ public static File resolveDefaultOutputBaseDir(File xsdFile) {
+ File resourcesDir = findAncestorByName(xsdFile, "resources");
+ if (resourcesDir != null) {
+ return resourcesDir;
+ }
+
+ File xsdParent = xsdFile == null ? null : xsdFile.getParentFile();
+ if (xsdParent == null) {
+ return null;
+ }
+ File siblingBase = xsdParent.getParentFile();
+ return siblingBase == null ? xsdParent : siblingBase;
+ }
+
+ public static File resolveNextMsgMapperDirectory(File baseDir) {
+ if (baseDir == null) {
+ return new File("msgmapper");
+ }
+ int suffix = 0;
+ while (true) {
+ String dirName = suffix == 0 ? "msgmapper" : "msgmapper" + suffix;
+ File candidate = new File(baseDir, dirName);
+ if (!candidate.exists()) {
+ return candidate;
+ }
+ suffix++;
+ }
+ }
+
+ public static File resolvePath(String path, File baseDir) {
+ String normalizedPath = trimToNull(path);
+ if (normalizedPath == null) {
+ return null;
+ }
+ File candidate = new File(normalizedPath);
+ if (candidate.isAbsolute() || baseDir == null) {
+ return candidate;
+ }
+ return new File(baseDir, normalizedPath);
+ }
+
+ public static String toRelativePath(File baseDir, File file) {
+ if (file == null) {
+ return "";
+ }
+ if (baseDir == null) {
+ return normalizeSeparators(file.getAbsolutePath());
+ }
+ try {
+ Path normalizedBase = baseDir.toPath().toAbsolutePath().normalize();
+ Path normalizedTarget = file.toPath().toAbsolutePath().normalize();
+ if (!normalizedTarget.startsWith(normalizedBase)) {
+ return normalizeSeparators(normalizedTarget.toString());
+ }
+ String relative = normalizedBase.relativize(normalizedTarget).toString();
+ return relative.isEmpty() ? "." : normalizeSeparators(relative);
+ } catch (Exception ex) {
+ return normalizeSeparators(file.getAbsolutePath());
+ }
+ }
+
+ public static String resolveRenderedXsdPath(File xsdFile, File moduleBaseDir) {
+ if (xsdFile == null) {
+ return "";
+ }
+ File resourcesDir = findAncestorByName(xsdFile, "resources");
+ File relativeBase = resourcesDir != null ? resourcesDir : moduleBaseDir;
+ if (relativeBase == null) {
+ return normalizeSeparators(xsdFile.getPath());
+ }
+ return toRelativePath(relativeBase, xsdFile);
+ }
+
+ public static File findAncestorByName(File node, String directoryName) {
+ String normalizedName = trimToNull(directoryName);
+ if (normalizedName == null) {
+ return null;
+ }
+ File current = toDirectory(node);
+ String targetName = normalizedName.toLowerCase(Locale.ROOT);
+ while (current != null) {
+ if (targetName.equals(current.getName().toLowerCase(Locale.ROOT))) {
+ return current;
+ }
+ current = current.getParentFile();
+ }
+ return null;
+ }
+
+ public static boolean isXmlFilePath(File file) {
+ if (file == null) {
+ return false;
+ }
+ return file.getName().toLowerCase(Locale.ROOT).endsWith(".xml");
+ }
+
+ private static File toDirectory(File file) {
+ if (file == null) {
+ return null;
+ }
+ if (file.isDirectory()) {
+ return file;
+ }
+ return file.getParentFile();
+ }
+
+ private static boolean containsModuleMarker(File directory) {
+ for (String marker : MODULE_MARKERS) {
+ if (new File(directory, marker).exists()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String normalizeSeparators(String path) {
+ if (path == null) {
+ return "";
+ }
+ return path.replace(File.separatorChar, '/');
+ }
+
+ private static String trimToNull(String text) {
+ if (text == null) {
+ return null;
+ }
+ String trimmed = text.trim();
+ if (trimmed.isEmpty()) {
+ return null;
+ }
+ return trimmed;
+ }
+}
diff --git a/src/main/java/com/dmiki/metaplugin/xom/XomXmlRenderer.java b/src/main/java/com/dmiki/metaplugin/xom/XomXmlRenderer.java
index 6e3f37d..2792142 100644
--- a/src/main/java/com/dmiki/metaplugin/xom/XomXmlRenderer.java
+++ b/src/main/java/com/dmiki/metaplugin/xom/XomXmlRenderer.java
@@ -6,8 +6,6 @@ import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
-import java.nio.file.Path;
-import java.nio.file.Paths;
public class XomXmlRenderer {
@@ -250,22 +248,8 @@ public class XomXmlRenderer {
if (rawXsdPath == null) {
return "";
}
- String projectBasePath = trimToNull(options.getProjectBasePath());
- if (projectBasePath == null) {
- return rawXsdPath;
- }
- try {
- Path resourcePath = Paths.get(projectBasePath, "src", "main", "resources")
- .toAbsolutePath()
- .normalize();
- Path xsdPath = Paths.get(rawXsdPath).toAbsolutePath().normalize();
- String relative = resourcePath.relativize(xsdPath).toString();
- if (relative.isEmpty()) {
- return rawXsdPath;
- }
- return relative.replace(File.separatorChar, '/');
- } catch (Exception ex) {
- return rawXsdPath;
- }
+ File xsdFile = new File(rawXsdPath);
+ File moduleBaseDir = XomPathStrategy.resolveModuleBaseDir(options.getProjectBasePath(), xsdFile);
+ return XomPathStrategy.resolveRenderedXsdPath(xsdFile, moduleBaseDir);
}
}
diff --git a/src/main/java/com/dmiki/metaplugin/xom/reverse/ReservedXomReverseDisplayService.java b/src/main/java/com/dmiki/metaplugin/xom/reverse/ReservedXomReverseDisplayService.java
index 31cd052..9a9f1a6 100644
--- a/src/main/java/com/dmiki/metaplugin/xom/reverse/ReservedXomReverseDisplayService.java
+++ b/src/main/java/com/dmiki/metaplugin/xom/reverse/ReservedXomReverseDisplayService.java
@@ -1,5 +1,6 @@
package com.dmiki.metaplugin.xom.reverse;
+import com.dmiki.metaplugin.xom.XomPathStrategy;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import com.dmiki.metaplugin.xsd.parser.XsdSchemaParser;
@@ -14,7 +15,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
public class ReservedXomReverseDisplayService implements XomReverseDisplayService {
@@ -29,14 +29,32 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
prepareForReplay(root);
applyReplay(root, mappingDocument.getResults());
- return new XomReverseDisplayResult(
- root,
- xsdFile,
- mappingDocument.getSchemaId(),
- mappingDocument.getResultMapType(),
- inferTopInput(mappingDocument.getCustomAttributeValues()),
- inferTopInput(mappingDocument.getPreProcessorValues())
- );
+ return buildDisplayResult(root, xsdFile, mappingDocument);
+ }
+
+ @Override
+ public XomReverseDisplayResult applyToCurrentTree(File mappingXmlFile,
+ String projectBasePath,
+ XsdConfigItem currentRoot,
+ XsdConfigItem targetNode) throws Exception {
+ if (currentRoot == null) {
+ throw new IllegalArgumentException("当前UI未加载XSD结构");
+ }
+ if (targetNode == null) {
+ throw new IllegalArgumentException("未选择要应用XOM的节点");
+ }
+
+ MappingDocument mappingDocument = parseMappingDocument(mappingXmlFile);
+ File xsdFile = resolveXsdFile(mappingDocument.getXsdPath(), mappingXmlFile, projectBasePath);
+
+ List targetPath = resolvePath(currentRoot, targetNode);
+ if (targetPath == null) {
+ throw new IllegalArgumentException("选中节点不属于当前XSD结构");
+ }
+
+ prepareForReplay(targetNode);
+ applyReplay(targetNode, resolveReplayPool(mappingDocument.getResults(), targetPath));
+ return buildDisplayResult(currentRoot, xsdFile, mappingDocument);
}
private MappingDocument parseMappingDocument(File mappingXmlFile) throws Exception {
@@ -76,6 +94,7 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
return;
}
item.setConfigMode(item.isEffectiveLeaf() ? XsdConfigMode.IGNORE : XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN);
+ item.setJavaProperty(null);
item.setCustomAttributeEnabled(false);
item.setCustomAttributeValue(null);
item.setPreProcessorEnabled(false);
@@ -85,13 +104,24 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
}
}
+ private XomReverseDisplayResult buildDisplayResult(XsdConfigItem root, File xsdFile, MappingDocument mappingDocument) {
+ return new XomReverseDisplayResult(
+ root,
+ xsdFile,
+ mappingDocument.getSchemaId(),
+ mappingDocument.getResultMapType(),
+ inferTopInput(mappingDocument.getCustomAttributeValues()),
+ inferTopInput(mappingDocument.getPreProcessorValues())
+ );
+ }
+
private void applyReplay(XsdConfigItem root, List topResults) {
if (root == null) {
return;
}
List pool = new ArrayList(topResults);
- ResultNode direct = removeFirstMatch(pool, root);
+ ResultNode direct = removeFirstDirectMatch(pool, root);
if (direct != null) {
applyDirectConfig(root, direct);
applyChildrenReplay(root, new ArrayList(direct.getChildren()));
@@ -114,7 +144,7 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
private void applyChildrenReplay(XsdConfigItem parent, List pool) {
for (XsdConfigItem child : parent.getChildren()) {
- ResultNode direct = removeFirstMatch(pool, child);
+ ResultNode direct = removeFirstDirectMatch(pool, child);
if (direct != null) {
applyDirectConfig(child, direct);
applyChildrenReplay(child, new ArrayList(direct.getChildren()));
@@ -156,10 +186,10 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
return false;
}
- private ResultNode removeFirstMatch(List pool, XsdConfigItem item) {
+ private ResultNode removeFirstDirectMatch(List pool, XsdConfigItem item) {
for (int i = 0; i < pool.size(); i++) {
ResultNode node = pool.get(i);
- if (matches(item, node)) {
+ if (matchesDirect(item, node)) {
pool.remove(i);
return node;
}
@@ -168,6 +198,14 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
}
private boolean matches(XsdConfigItem item, ResultNode node) {
+ return matches(item, node, true);
+ }
+
+ private boolean matchesDirect(XsdConfigItem item, ResultNode node) {
+ return matches(item, node, item != null && item.isEffectiveLeaf());
+ }
+
+ private boolean matches(XsdConfigItem item, ResultNode node, boolean allowTailMatch) {
if (item == null || node == null) {
return false;
}
@@ -183,7 +221,101 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
if (actual == null) {
return false;
}
- return expected.equals(actual) || expected.equalsIgnoreCase(actual);
+ return matchesName(expected, actual, allowTailMatch);
+ }
+
+ private boolean matchesName(String expected, String actual, boolean allowTailMatch) {
+ String normalizedExpected = trimToNull(expected);
+ String normalizedActual = trimToNull(actual);
+ if (normalizedExpected == null || normalizedActual == null) {
+ return false;
+ }
+ if (normalizedExpected.equals(normalizedActual) || normalizedExpected.equalsIgnoreCase(normalizedActual)) {
+ return true;
+ }
+ if (!allowTailMatch) {
+ return false;
+ }
+ int index = normalizedActual.lastIndexOf(':');
+ if (index < 0 || index >= normalizedActual.length() - 1) {
+ return false;
+ }
+ String tail = trimToNull(normalizedActual.substring(index + 1));
+ return tail != null && (normalizedExpected.equals(tail) || normalizedExpected.equalsIgnoreCase(tail));
+ }
+
+ private List resolveReplayPool(List topResults, List targetPath) {
+ List ancestors = new ArrayList();
+ if (targetPath != null && targetPath.size() > 1) {
+ ancestors.addAll(targetPath.subList(0, targetPath.size() - 1));
+ }
+
+ List pools = new ArrayList();
+ collectReplayPools(topResults, new ArrayList(), pools);
+
+ ReplayPool best = null;
+ int bestScore = -1;
+ for (ReplayPool pool : pools) {
+ int score = matchAncestorSuffix(ancestors, pool.getAncestorPath());
+ if (score > bestScore) {
+ best = pool;
+ bestScore = score;
+ }
+ }
+ if (best == null) {
+ return new ArrayList();
+ }
+ return new ArrayList(best.getNodes());
+ }
+
+ private void collectReplayPools(List nodes, List ancestors, List pools) {
+ pools.add(new ReplayPool(nodes, ancestors));
+ if (nodes == null || nodes.isEmpty()) {
+ return;
+ }
+ for (ResultNode node : nodes) {
+ List nextAncestors = new ArrayList(ancestors);
+ nextAncestors.add(node);
+ collectReplayPools(node.getChildren(), nextAncestors, pools);
+ }
+ }
+
+ private int matchAncestorSuffix(List ancestors, List candidatePath) {
+ if (candidatePath == null || candidatePath.isEmpty()) {
+ return 0;
+ }
+ if (ancestors == null || candidatePath.size() > ancestors.size()) {
+ return -1;
+ }
+ int offset = ancestors.size() - candidatePath.size();
+ for (int i = 0; i < candidatePath.size(); i++) {
+ if (!matches(ancestors.get(offset + i), candidatePath.get(i))) {
+ return -1;
+ }
+ }
+ return candidatePath.size();
+ }
+
+ private List resolvePath(XsdConfigItem root, XsdConfigItem targetNode) {
+ List path = new ArrayList();
+ return collectPath(root, targetNode, path) ? path : null;
+ }
+
+ private boolean collectPath(XsdConfigItem current, XsdConfigItem target, List path) {
+ if (current == null || target == null || path == null) {
+ return false;
+ }
+ path.add(current);
+ if (current == target) {
+ return true;
+ }
+ for (XsdConfigItem child : current.getChildren()) {
+ if (collectPath(child, target, path)) {
+ return true;
+ }
+ }
+ path.remove(path.size() - 1);
+ return false;
}
private void applyDirectConfig(XsdConfigItem item, ResultNode node) {
@@ -213,6 +345,7 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
}
List candidates = new ArrayList();
+ File moduleBaseDir = XomPathStrategy.resolveModuleBaseDir(projectBasePath, mappingXmlFile);
File declared = new File(xsdPath);
if (declared.isAbsolute()) {
candidates.add(declared);
@@ -223,16 +356,14 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
candidates.add(new File(mappingParent, xsdPath));
}
- File resourcesRoot = findAncestorByName(mappingParent, "resources");
+ File resourcesRoot = XomPathStrategy.findAncestorByName(mappingParent, "resources");
if (resourcesRoot != null) {
candidates.add(new File(resourcesRoot, xsdPath));
}
- String normalizedBasePath = trimToNull(projectBasePath);
- if (normalizedBasePath != null) {
- File projectBaseDir = new File(normalizedBasePath);
- candidates.add(new File(projectBaseDir, xsdPath));
- candidates.add(new File(new File(new File(new File(projectBaseDir, "src"), "main"), "resources"), xsdPath));
+ if (moduleBaseDir != null) {
+ candidates.add(new File(moduleBaseDir, xsdPath));
+ candidates.add(new File(new File(new File(new File(moduleBaseDir, "src"), "main"), "resources"), xsdPath));
}
Set dedup = new LinkedHashSet();
@@ -253,21 +384,6 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
throw new IllegalArgumentException("根据xsdPath未找到XSD文件: " + xsdPath);
}
- private File findAncestorByName(File node, String directoryName) {
- if (directoryName == null) {
- return null;
- }
- String normalizedName = directoryName.toLowerCase(Locale.ROOT);
- File current = node;
- while (current != null) {
- if (normalizedName.equals(current.getName().toLowerCase(Locale.ROOT))) {
- return current;
- }
- current = current.getParentFile();
- }
- return null;
- }
-
private List parseResultChildren(Element parent,
Set customAttributeValues,
Set preProcessorValues) {
@@ -482,4 +598,22 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
return children;
}
}
+
+ private static final class ReplayPool {
+ private final List nodes;
+ private final List ancestorPath;
+
+ private ReplayPool(List nodes, List ancestorPath) {
+ this.nodes = nodes == null ? new ArrayList() : new ArrayList(nodes);
+ this.ancestorPath = ancestorPath == null ? new ArrayList() : new ArrayList(ancestorPath);
+ }
+
+ private List getNodes() {
+ return nodes;
+ }
+
+ private List getAncestorPath() {
+ return ancestorPath;
+ }
+ }
}
diff --git a/src/main/java/com/dmiki/metaplugin/xom/reverse/XomReverseDisplayService.java b/src/main/java/com/dmiki/metaplugin/xom/reverse/XomReverseDisplayService.java
index d46014f..d80aee3 100644
--- a/src/main/java/com/dmiki/metaplugin/xom/reverse/XomReverseDisplayService.java
+++ b/src/main/java/com/dmiki/metaplugin/xom/reverse/XomReverseDisplayService.java
@@ -1,8 +1,15 @@
package com.dmiki.metaplugin.xom.reverse;
+import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
+
import java.io.File;
public interface XomReverseDisplayService {
XomReverseDisplayResult loadForDisplay(File mappingXmlFile, String projectBasePath) throws Exception;
+
+ XomReverseDisplayResult applyToCurrentTree(File mappingXmlFile,
+ String projectBasePath,
+ XsdConfigItem currentRoot,
+ XsdConfigItem targetNode) throws Exception;
}
diff --git a/src/main/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolver.java b/src/main/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolver.java
new file mode 100644
index 0000000..da12612
--- /dev/null
+++ b/src/main/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolver.java
@@ -0,0 +1,321 @@
+package com.dmiki.metaplugin.xsd.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public final class JavaPackageResolver {
+ private static final String[] PREFERRED_PACKAGES = {"entity", "domain", "message"};
+
+ private JavaPackageResolver() {
+ }
+
+ public static String resolveDefaultPackage(File moduleBaseDir) {
+ List sourceRoots = collectJavaSourceRoots(moduleBaseDir);
+ if (sourceRoots.isEmpty()) {
+ return null;
+ }
+
+ String preferredPackage = resolvePreferredPackage(sourceRoots);
+ if (preferredPackage != null) {
+ return preferredPackage;
+ }
+
+ return inferModulePackage(sourceRoots);
+ }
+
+ static List collectJavaSourceRoots(File moduleBaseDir) {
+ if (moduleBaseDir == null || !moduleBaseDir.isDirectory()) {
+ return Collections.emptyList();
+ }
+
+ Map sourceRoots = new LinkedHashMap();
+ registerSourceRoot(sourceRoots, new File(moduleBaseDir, "src/main/java"));
+ registerSourceRoot(sourceRoots, new File(moduleBaseDir, "src/test/java"));
+ registerSourceRoot(sourceRoots, new File(moduleBaseDir, "src/java"));
+
+ File srcDir = new File(moduleBaseDir, "src");
+ for (File child : listDirectories(srcDir)) {
+ registerSourceRoot(sourceRoots, new File(child, "java"));
+ }
+
+ return new ArrayList(sourceRoots.values());
+ }
+
+ private static void registerSourceRoot(Map sourceRoots, File sourceRoot) {
+ if (sourceRoot == null || !sourceRoot.isDirectory()) {
+ return;
+ }
+ String key;
+ try {
+ key = sourceRoot.getCanonicalPath();
+ sourceRoot = sourceRoot.getCanonicalFile();
+ } catch (IOException ex) {
+ key = sourceRoot.getAbsolutePath();
+ sourceRoot = sourceRoot.getAbsoluteFile();
+ }
+ sourceRoots.put(key, sourceRoot);
+ }
+
+ private static String resolvePreferredPackage(List sourceRoots) {
+ for (String preferredPackageName : PREFERRED_PACKAGES) {
+ List candidates = new ArrayList();
+ for (File sourceRoot : sourceRoots) {
+ collectPackageDirectoriesByName(sourceRoot, preferredPackageName, candidates);
+ }
+ String bestMatch = chooseBestPackage(candidates);
+ if (bestMatch != null) {
+ return bestMatch;
+ }
+ }
+ return null;
+ }
+
+ private static void collectPackageDirectoriesByName(File sourceRoot, String directoryName, List candidates) {
+ if (sourceRoot == null || directoryName == null) {
+ return;
+ }
+ Deque stack = new ArrayDeque();
+ stack.push(sourceRoot);
+ while (!stack.isEmpty()) {
+ File current = stack.pop();
+ List children = listDirectories(current);
+ for (int i = children.size() - 1; i >= 0; i--) {
+ File child = children.get(i);
+ if (directoryName.equals(child.getName())) {
+ String packageName = toPackageName(sourceRoot, child);
+ if (packageName != null) {
+ candidates.add(packageName);
+ }
+ }
+ stack.push(child);
+ }
+ }
+ }
+
+ private static String inferModulePackage(List sourceRoots) {
+ Set packageNames = new LinkedHashSet();
+ for (File sourceRoot : sourceRoots) {
+ collectJavaFilePackages(sourceRoot, packageNames);
+ String chainedPackage = inferSinglePathPackage(sourceRoot);
+ if (chainedPackage != null) {
+ packageNames.add(chainedPackage);
+ }
+ }
+
+ if (packageNames.isEmpty()) {
+ return null;
+ }
+
+ String commonPrefix = longestCommonPackagePrefix(packageNames);
+ if (commonPrefix != null) {
+ return commonPrefix;
+ }
+ return chooseBestPackage(new ArrayList(packageNames));
+ }
+
+ private static void collectJavaFilePackages(File sourceRoot, Set packageNames) {
+ Deque stack = new ArrayDeque();
+ stack.push(sourceRoot);
+ while (!stack.isEmpty()) {
+ File current = stack.pop();
+ for (File child : listChildren(current)) {
+ if (child.isDirectory()) {
+ if (isSkippableDirectory(child)) {
+ continue;
+ }
+ stack.push(child);
+ continue;
+ }
+ if (!child.isFile() || !child.getName().endsWith(".java")) {
+ continue;
+ }
+ String packageName = toPackageName(sourceRoot, child.getParentFile());
+ if (packageName != null) {
+ packageNames.add(packageName);
+ }
+ }
+ }
+ }
+
+ private static String inferSinglePathPackage(File sourceRoot) {
+ List segments = new ArrayList();
+ File current = sourceRoot;
+ while (current != null && current.isDirectory()) {
+ if (containsJavaFiles(current)) {
+ break;
+ }
+ List children = listPackageDirectories(current);
+ if (children.size() != 1) {
+ break;
+ }
+ File child = children.get(0);
+ segments.add(child.getName());
+ current = child;
+ }
+ if (segments.isEmpty()) {
+ return null;
+ }
+ return joinSegments(segments);
+ }
+
+ private static boolean containsJavaFiles(File directory) {
+ for (File child : listChildren(directory)) {
+ if (child.isFile() && child.getName().endsWith(".java")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String longestCommonPackagePrefix(Set packageNames) {
+ if (packageNames.isEmpty()) {
+ return null;
+ }
+ String[] commonSegments = null;
+ for (String packageName : packageNames) {
+ if (packageName == null) {
+ continue;
+ }
+ String[] segments = packageName.split("\\.");
+ if (commonSegments == null) {
+ commonSegments = segments;
+ continue;
+ }
+ int length = Math.min(commonSegments.length, segments.length);
+ int index = 0;
+ while (index < length && commonSegments[index].equals(segments[index])) {
+ index++;
+ }
+ if (index == 0) {
+ return null;
+ }
+ commonSegments = Arrays.copyOf(commonSegments, index);
+ }
+ if (commonSegments == null || commonSegments.length == 0) {
+ return null;
+ }
+ return joinSegments(Arrays.asList(commonSegments));
+ }
+
+ private static String chooseBestPackage(List packageNames) {
+ if (packageNames == null || packageNames.isEmpty()) {
+ return null;
+ }
+ Collections.sort(packageNames, new Comparator() {
+ @Override
+ public int compare(String left, String right) {
+ int leftDepth = left.split("\\.").length;
+ int rightDepth = right.split("\\.").length;
+ if (leftDepth != rightDepth) {
+ return Integer.compare(leftDepth, rightDepth);
+ }
+ if (left.length() != right.length()) {
+ return Integer.compare(left.length(), right.length());
+ }
+ return left.compareTo(right);
+ }
+ });
+ return packageNames.get(0);
+ }
+
+ private static String toPackageName(File sourceRoot, File directory) {
+ if (sourceRoot == null || directory == null) {
+ return null;
+ }
+ String relativePath;
+ try {
+ relativePath = sourceRoot.toPath().relativize(directory.toPath()).toString();
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ if (relativePath.isEmpty()) {
+ return null;
+ }
+ String[] segments = relativePath.replace('\\', '/').split("/");
+ for (String segment : segments) {
+ if (!isValidPackageSegment(segment)) {
+ return null;
+ }
+ }
+ return joinSegments(Arrays.asList(segments));
+ }
+
+ private static List listDirectories(File directory) {
+ List directories = new ArrayList();
+ for (File child : listChildren(directory)) {
+ if (child.isDirectory() && !isSkippableDirectory(child)) {
+ directories.add(child);
+ }
+ }
+ Collections.sort(directories, new Comparator() {
+ @Override
+ public int compare(File left, File right) {
+ return left.getName().compareTo(right.getName());
+ }
+ });
+ return directories;
+ }
+
+ private static List listPackageDirectories(File directory) {
+ List directories = new ArrayList();
+ for (File child : listDirectories(directory)) {
+ if (isValidPackageSegment(child.getName())) {
+ directories.add(child);
+ }
+ }
+ return directories;
+ }
+
+ private static File[] listChildren(File directory) {
+ if (directory == null || !directory.isDirectory()) {
+ return new File[0];
+ }
+ File[] children = directory.listFiles();
+ return children == null ? new File[0] : children;
+ }
+
+ private static boolean isSkippableDirectory(File directory) {
+ String name = directory.getName();
+ return name.startsWith(".")
+ || "build".equals(name)
+ || "out".equals(name)
+ || "target".equals(name);
+ }
+
+ private static boolean isValidPackageSegment(String segment) {
+ if (segment == null || segment.isEmpty()) {
+ return false;
+ }
+ if (!Character.isJavaIdentifierStart(segment.charAt(0))) {
+ return false;
+ }
+ for (int i = 1; i < segment.length(); i++) {
+ if (!Character.isJavaIdentifierPart(segment.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static String joinSegments(List segments) {
+ StringBuilder builder = new StringBuilder();
+ for (String segment : segments) {
+ if (builder.length() > 0) {
+ builder.append('.');
+ }
+ builder.append(segment);
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialog.java b/src/main/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialog.java
index ce60af6..34d5229 100644
--- a/src/main/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialog.java
+++ b/src/main/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialog.java
@@ -5,18 +5,23 @@ import com.dmiki.metaplugin.xom.XomGenerationDefaults;
import com.dmiki.metaplugin.xom.XomGenerationException;
import com.dmiki.metaplugin.xom.XomGenerationResult;
import com.dmiki.metaplugin.xom.XomGenerationService;
+import com.dmiki.metaplugin.xom.XomPathStrategy;
import com.dmiki.metaplugin.xom.reverse.ReservedXomReverseDisplayService;
import com.dmiki.metaplugin.xom.reverse.XomReverseDisplayResult;
import com.dmiki.metaplugin.xom.reverse.XomReverseDisplayService;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import com.dmiki.metaplugin.xsd.parser.XsdSchemaParser;
+import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBLabel;
@@ -76,15 +81,19 @@ import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class XsdConfigDialog extends DialogWrapper {
- private static final int TOGGLE_INPUT_GAP = 14;
+ private static final int TOGGLE_INPUT_GAP = 10;
+ private static final int TOGGLE_INPUT_OUTER_HORIZONTAL_PADDING = 4;
+ private static final int TOGGLE_INPUT_INNER_HORIZONTAL_PADDING = 3;
+ private static final int TOGGLE_INPUT_TEXT_HORIZONTAL_PADDING = 2;
+ private static final int TOGGLE_INPUT_TEXT_WIDTH = 60;
+ private static final int TOGGLE_INPUT_CHECKBOX_MIN_SIZE = 20;
+ private static final String DEFAULT_RESULT_MAP_PACKAGE = "com.example.message";
private final Project project;
private final XsdSchemaParser parser = new XsdSchemaParser();
private final XomGenerationService xomGenerationService = new XomGenerationService();
@@ -109,6 +118,7 @@ public class XsdConfigDialog extends DialogWrapper {
private MouseAdapter outsideClickEditStopper;
private XsdConfigItem currentRoot;
private File currentXsdFile;
+ private String currentModuleBasePath;
private String lastAutoResultMapType;
public XsdConfigDialog(@Nullable Project project) {
@@ -154,7 +164,7 @@ public class XsdConfigDialog extends DialogWrapper {
installOutsideClickEditTermination(panel);
- panel.setPreferredSize(new Dimension(1220, 720));
+ panel.setPreferredSize(new Dimension(1280, 720));
setActionButtonsEnabled(false);
return panel;
}
@@ -213,7 +223,7 @@ public class XsdConfigDialog extends DialogWrapper {
row1.setOpaque(false);
row1.add(createOptionLabel("报文类型:"));
- msgTypeField = createOptionField(180);
+ msgTypeField = createOptionField(130);
row1.add(msgTypeField);
row1.add(createOptionLabel("包名:"));
@@ -222,6 +232,16 @@ public class XsdConfigDialog extends DialogWrapper {
installResultMapTypeSync();
syncResultMapTypeFromMsgType(true);
+ row1.add(createOptionLabel("自定义属性:"));
+ customAttributeField = createOptionField(120);
+ customAttributeField.setText("sign");
+ row1.add(customAttributeField);
+
+ row1.add(createOptionLabel("预处理属性:"));
+ preProcessorField = createOptionField(150);
+ preProcessorField.setText("");
+ row1.add(preProcessorField);
+
row1.add(createOptionLabel("输出目录:"));
outputPathField = new TextFieldWithBrowseButton();
outputPathField.setPreferredSize(new Dimension(340, 32));
@@ -236,22 +256,7 @@ public class XsdConfigDialog extends DialogWrapper {
});
row1.add(outputPathField);
- JBPanel row2 = new JBPanel(new HorizontalLayout(8));
- row2.setOpaque(false);
- row2.setBorder(new EmptyBorder(6, 0, 0, 0));
-
- row2.add(createOptionLabel("自定义属性:"));
- customAttributeField = createOptionField(180);
- customAttributeField.setText("sign");
- row2.add(customAttributeField);
-
- row2.add(createOptionLabel("预处理属性:"));
- preProcessorField = createOptionField(220);
- preProcessorField.setText("");
- row2.add(preProcessorField);
-
container.add(row1);
- container.add(row2);
return container;
}
@@ -304,7 +309,8 @@ public class XsdConfigDialog extends DialogWrapper {
}
private String buildDefaultResultMapType() {
- return "com.example.message";
+ String packageName = JavaPackageResolver.resolveDefaultPackage(moduleBaseDir());
+ return packageName == null ? DEFAULT_RESULT_MAP_PACKAGE : packageName;
}
private void styleTextField(JTextField textField) {
@@ -436,7 +442,7 @@ public class XsdConfigDialog extends DialogWrapper {
try {
XomReverseDisplayResult reverseResult = xomReverseDisplayService.loadForDisplay(
xomFile,
- project == null ? null : project.getBasePath());
+ resolveModuleBasePath(xomFile));
File xsdFile = reverseResult.getXsdFile();
XsdConfigItem root = reverseResult.getRoot();
@@ -453,6 +459,7 @@ public class XsdConfigDialog extends DialogWrapper {
private void displayTree(File xsdFile, XsdConfigItem root) {
currentRoot = root;
currentXsdFile = xsdFile;
+ currentModuleBasePath = resolveModuleBasePath(xsdFile);
applyGenerationDefaults(xsdFile);
DefaultMutableTreeNode swingRoot = XsdTreeTableModel.toSwingTree(root);
@@ -483,7 +490,7 @@ public class XsdConfigDialog extends DialogWrapper {
}
if (outputPathField != null && xomFile != null) {
File parent = xomFile.getParentFile();
- outputPathField.setText(toProjectRelativePath(parent == null ? xomFile : parent));
+ outputPathField.setText(toModuleRelativePath(parent == null ? xomFile : parent));
}
if (customAttributeField != null) {
customAttributeField.setText(trimToEmpty(reverseResult.getCustomAttribute()));
@@ -610,6 +617,12 @@ public class XsdConfigDialog extends DialogWrapper {
private JPopupMenu createNodeContextMenu(XsdConfigItem selectedItem) {
JPopupMenu menu = new JPopupMenu();
+ JMenuItem applyItem = new JMenuItem("应用已存在的XOM");
+ applyItem.addActionListener(event -> applyExistingXomToNode(selectedItem));
+ menu.add(applyItem);
+
+ menu.addSeparator();
+
JMenuItem previewItem = new JMenuItem("预览选中节点XOM");
previewItem.addActionListener(event -> previewXomForNode(selectedItem));
menu.add(previewItem);
@@ -621,6 +634,86 @@ public class XsdConfigDialog extends DialogWrapper {
return menu;
}
+ private void applyExistingXomToNode(XsdConfigItem selectedItem) {
+ if (currentRoot == null || currentXsdFile == null || selectedItem == null) {
+ Messages.showWarningDialog(project, "请先解析XSD并选择节点", "提示");
+ return;
+ }
+
+ stopTreeTableEditing();
+ File xomFile = chooseExistingXomFile();
+ if (xomFile == null) {
+ return;
+ }
+
+ try {
+ XomReverseDisplayResult reverseResult = xomReverseDisplayService.applyToCurrentTree(
+ xomFile,
+ resolveModuleBasePath(xomFile),
+ currentRoot,
+ selectedItem);
+ if (selectedItem == currentRoot) {
+ applyReplayedOptions(reverseResult, xomFile);
+ }
+ refreshTreeTableAfterReplay();
+
+ String nodeName = trimToNull(selectedItem.getXmlName());
+ Messages.showInfoMessage(project,
+ "XOM配置已应用到节点: " + (nodeName == null ? "(未命名节点)" : nodeName),
+ "完成");
+ } catch (Exception ex) {
+ Messages.showErrorDialog(project, "应用XOM失败: " + ex.getMessage(), "错误");
+ }
+ }
+
+ private File chooseExistingXomFile() {
+ FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor();
+ descriptor.withFileFilter(virtualFile -> {
+ String extension = virtualFile.getExtension();
+ return "xml".equalsIgnoreCase(extension) || "xom".equalsIgnoreCase(extension);
+ });
+ descriptor.setTitle("选择已存在的XOM文件");
+ descriptor.setDescription("选择一个 .xml 或 .xom 文件,并将配置应用到当前选中节点");
+
+ File initialFile = resolveInitialXomSelection();
+ VirtualFile initialVirtualFile = initialFile == null
+ ? null
+ : LocalFileSystem.getInstance().refreshAndFindFileByIoFile(initialFile);
+ VirtualFile selected = FileChooser.chooseFile(descriptor, project, initialVirtualFile);
+ return selected == null ? null : new File(selected.getPath());
+ }
+
+ private File resolveInitialXomSelection() {
+ String outputPath = valueOf(outputPathField);
+ if (outputPath != null) {
+ File outputDirectory = resolveOutputDirectory(outputPath);
+ if (outputDirectory != null && outputDirectory.exists()) {
+ return outputDirectory;
+ }
+ }
+ if (currentXsdFile != null) {
+ File parent = currentXsdFile.getParentFile();
+ if (parent != null && parent.exists()) {
+ return parent;
+ }
+ }
+ File moduleBaseDir = moduleBaseDir();
+ if (moduleBaseDir != null && moduleBaseDir.exists()) {
+ return moduleBaseDir;
+ }
+ return null;
+ }
+
+ private void refreshTreeTableAfterReplay() {
+ if (treeTable != null) {
+ treeTable.revalidate();
+ treeTable.repaint();
+ }
+ if (currentRoot != null) {
+ refreshStats(currentRoot);
+ }
+ }
+
private void installOutsideClickEditTermination(JComponent rootComponent) {
if (outsideClickEditStopper != null) {
return;
@@ -701,6 +794,23 @@ public class XsdConfigDialog extends DialogWrapper {
});
}
+ static Dimension toggleCheckBoxSize(@Nullable Dimension preferredSize) {
+ int width = preferredSize == null ? 0 : preferredSize.width;
+ int height = preferredSize == null ? 0 : preferredSize.height;
+ int size = Math.max(TOGGLE_INPUT_CHECKBOX_MIN_SIZE, Math.max(width, height));
+ return new Dimension(size, size);
+ }
+
+ private static void applyToggleCheckBoxSizing(JCheckBox checkBox) {
+ if (checkBox == null) {
+ return;
+ }
+ Dimension size = toggleCheckBoxSize(checkBox.getPreferredSize());
+ checkBox.setPreferredSize(size);
+ checkBox.setMinimumSize(size);
+ checkBox.setMaximumSize(size);
+ }
+
private XsdConfigItem itemOfRow(JTable table, int row) {
if (!(table instanceof TreeTable)) {
return null;
@@ -713,6 +823,22 @@ public class XsdConfigDialog extends DialogWrapper {
return itemOf(path.getLastPathComponent());
}
+ static boolean shouldRenderConfigField(JTable table, int row) {
+ if (!(table instanceof TreeTable)) {
+ return true;
+ }
+ JTree tree = ((TreeTable) table).getTree();
+ TreePath path = tree.getPathForRow(row);
+ if (path == null) {
+ return true;
+ }
+ Object node = path.getLastPathComponent();
+ if (!(node instanceof DefaultMutableTreeNode)) {
+ return true;
+ }
+ return ((DefaultMutableTreeNode) node).getParent() != null;
+ }
+
private void installTextEditor(TableColumn column) {
column.setCellEditor(new InputLikeTextCellEditor());
}
@@ -794,25 +920,9 @@ public class XsdConfigDialog extends DialogWrapper {
}
private String resolveDefaultOutputPath(File xsdFile) {
- if (project == null || project.getBasePath() == null) {
- File parent = xsdFile.getParentFile();
- return parent == null ? xsdFile.getAbsolutePath() : parent.getAbsolutePath();
- }
- File resourcesDir = new File(project.getBasePath(), "src/main/resources");
- File outputDir = resolveNextMsgMapperDirectory(resourcesDir);
- return toProjectRelativePath(outputDir);
- }
-
- private File resolveNextMsgMapperDirectory(File resourcesDir) {
- int suffix = 0;
- while (true) {
- String dirName = suffix == 0 ? "msgmapper" : "msgmapper" + suffix;
- File candidate = new File(resourcesDir, dirName);
- if (!candidate.exists()) {
- return candidate;
- }
- suffix++;
- }
+ File outputBaseDir = XomPathStrategy.resolveDefaultOutputBaseDir(xsdFile);
+ File outputDir = XomPathStrategy.resolveNextMsgMapperDirectory(outputBaseDir);
+ return toModuleRelativePath(outputDir);
}
private void generateXomFile() {
@@ -830,8 +940,8 @@ public class XsdConfigDialog extends DialogWrapper {
File javaEntityFile = result.getJavaEntityFile();
Messages.showInfoMessage(project,
"XOM生成成功:\n"
- + "XML: " + toProjectRelativePath(result.getMappingXmlFile())
- + "\nJAVA: " + (javaEntityFile == null ? "未生成(包名为空)" : toProjectRelativePath(javaEntityFile)),
+ + "XML: " + toModuleRelativePath(result.getMappingXmlFile())
+ + "\nJAVA: " + (javaEntityFile == null ? "未生成(包名为空)" : toModuleRelativePath(javaEntityFile)),
"完成");
} catch (XomGenerationException ex) {
Messages.showErrorDialog(project, "XOM生成失败: " + ex.getMessage(), "错误");
@@ -886,8 +996,8 @@ public class XsdConfigDialog extends DialogWrapper {
Messages.showInfoMessage(project,
"节点XOM生成成功:\n"
+ "节点: " + (nodeName == null ? "(未命名节点)" : nodeName)
- + "\nXML: " + toProjectRelativePath(result.getMappingXmlFile())
- + "\nJAVA: " + (javaEntityFile == null ? "未生成(包名为空)" : toProjectRelativePath(javaEntityFile)),
+ + "\nXML: " + toModuleRelativePath(result.getMappingXmlFile())
+ + "\nJAVA: " + (javaEntityFile == null ? "未生成(包名为空)" : toModuleRelativePath(javaEntityFile)),
"完成");
} catch (XomGenerationException ex) {
Messages.showErrorDialog(project, "节点XOM生成失败: " + ex.getMessage(), "错误");
@@ -914,7 +1024,7 @@ public class XsdConfigDialog extends DialogWrapper {
boolean hasJavaPackage = hasPackageName(packageName);
String outputPath = resolveOutputFilePath(outputDirectoryPath, outputFileName);
return new XomGenerateOptions(
- project == null ? null : project.getBasePath(),
+ currentModuleBasePath,
null,
valueOf(msgTypeField),
resultMapType,
@@ -998,13 +1108,13 @@ public class XsdConfigDialog extends DialogWrapper {
if (fileName == null) {
fileName = defaultOutputName();
}
- return toProjectRelativePath(new File(directory, fileName));
+ return toModuleRelativePath(new File(directory, fileName));
}
private File resolveOutputDirectory(String outputDirectoryPath) {
- File candidate = resolvePathAgainstProjectBase(outputDirectoryPath);
+ File candidate = resolvePathAgainstModuleBase(outputDirectoryPath);
if (candidate != null) {
- if (isXmlFilePath(candidate)) {
+ if (XomPathStrategy.isXmlFilePath(candidate)) {
File parent = candidate.getParentFile();
if (parent != null) {
return parent;
@@ -1013,25 +1123,14 @@ public class XsdConfigDialog extends DialogWrapper {
return candidate;
}
if (currentXsdFile != null) {
- if (project != null && trimToNull(project.getBasePath()) != null) {
- File resourcesDir = new File(project.getBasePath(), "src/main/resources");
- return resolveNextMsgMapperDirectory(resourcesDir);
- }
- File parent = currentXsdFile.getParentFile();
- if (parent != null) {
- return parent;
+ File outputBaseDir = XomPathStrategy.resolveDefaultOutputBaseDir(currentXsdFile);
+ if (outputBaseDir != null) {
+ return XomPathStrategy.resolveNextMsgMapperDirectory(outputBaseDir);
}
}
return new File("msgmapper");
}
- private boolean isXmlFilePath(File file) {
- if (file == null) {
- return false;
- }
- return file.getName().toLowerCase(Locale.ROOT).endsWith(".xml");
- }
-
private String valueOf(TextFieldWithBrowseButton field) {
if (field == null) {
return null;
@@ -1054,46 +1153,57 @@ public class XsdConfigDialog extends DialogWrapper {
if (path == null) {
return;
}
- outputPathField.setText(toProjectRelativePath(resolveOutputDirectory(path)));
+ outputPathField.setText(toModuleRelativePath(resolveOutputDirectory(path)));
}
- private File resolvePathAgainstProjectBase(String path) {
- String normalizedPath = trimToNull(path);
- if (normalizedPath == null) {
- return null;
- }
- File candidate = new File(normalizedPath);
- if (candidate.isAbsolute()) {
- return candidate;
- }
- String projectBasePath = project == null ? null : trimToNull(project.getBasePath());
- if (projectBasePath == null) {
- return candidate;
- }
- return new File(projectBasePath, normalizedPath);
+ private File resolvePathAgainstModuleBase(String path) {
+ return XomPathStrategy.resolvePath(path, moduleBaseDir());
}
- private String toProjectRelativePath(File file) {
+ private String toModuleRelativePath(File file) {
if (file == null) {
return "";
}
- String projectBasePath = project == null ? null : trimToNull(project.getBasePath());
- if (projectBasePath == null) {
+ File moduleBaseDir = moduleBaseDir();
+ if (moduleBaseDir == null) {
return file.getAbsolutePath();
}
- try {
- Path basePath = Paths.get(projectBasePath).toAbsolutePath().normalize();
- Path targetPath = file.toPath().toAbsolutePath().normalize();
- if (targetPath.startsWith(basePath)) {
- String relativePath = basePath.relativize(targetPath).toString();
- if (!relativePath.isEmpty()) {
- return relativePath.replace(File.separatorChar, '/');
- }
- }
- } catch (Exception ignore) {
- return file.getAbsolutePath();
+ return XomPathStrategy.toRelativePath(moduleBaseDir, file);
+ }
+
+ private File moduleBaseDir() {
+ String basePath = trimToNull(currentModuleBasePath);
+ return basePath == null ? null : new File(basePath);
+ }
+
+ private String resolveModuleBasePath(File file) {
+ File moduleBaseDir = resolveModuleBaseDir(file);
+ return moduleBaseDir == null ? null : moduleBaseDir.getAbsolutePath();
+ }
+
+ private File resolveModuleBaseDir(File file) {
+ File contentRoot = findContentRoot(file);
+ if (contentRoot != null) {
+ return contentRoot;
}
- return file.getAbsolutePath();
+ return XomPathStrategy.inferModuleBaseDir(file);
+ }
+
+ private File findContentRoot(File file) {
+ if (project == null || file == null) {
+ return null;
+ }
+ VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
+ if (virtualFile == null) {
+ return null;
+ }
+ VirtualFile contentRoot = ProjectRootManager.getInstance(project)
+ .getFileIndex()
+ .getContentRootForFile(virtualFile);
+ if (contentRoot == null) {
+ return null;
+ }
+ return new File(contentRoot.getPath());
}
private String removeExtension(String fileName) {
@@ -1388,6 +1498,12 @@ public class XsdConfigDialog extends DialogWrapper {
int row,
int column) {
setBackground(rowBackground(row, isSelected));
+ boolean showField = shouldRenderConfigField(table, row);
+ fieldPanel.setVisible(showField);
+ if (!showField) {
+ textLabel.setText("");
+ return this;
+ }
String text = value == null ? "" : value.toString();
textLabel.setText(text);
@@ -1441,6 +1557,12 @@ public class XsdConfigDialog extends DialogWrapper {
int row,
int column) {
setBackground(rowBackground(row, isSelected));
+ boolean showField = shouldRenderConfigField(table, row);
+ fieldPanel.setVisible(showField);
+ if (!showField) {
+ textLabel.setText("");
+ return this;
+ }
fieldPanel.setBackground(isSelected ? Palette.FIELD_BG_SELECTED : Palette.FIELD_BG);
textLabel.setText(value == null ? "" : value.toString());
return this;
@@ -1627,14 +1749,14 @@ public class XsdConfigDialog extends DialogWrapper {
ToggleInputCellRenderer() {
setLayout(new BorderLayout());
setOpaque(true);
- setBorder(new EmptyBorder(4, 8, 4, 8));
+ setBorder(new EmptyBorder(4, TOGGLE_INPUT_OUTER_HORIZONTAL_PADDING, 4, TOGGLE_INPUT_OUTER_HORIZONTAL_PADDING));
fieldPanel = new JPanel(new GridBagLayout());
fieldPanel.setOpaque(true);
fieldPanel.setBackground(Palette.FIELD_BG);
fieldPanel.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(Palette.FIELD_BORDER, 1, true),
- new EmptyBorder(0, 6, 0, 6)));
+ new EmptyBorder(0, TOGGLE_INPUT_INNER_HORIZONTAL_PADDING, 0, TOGGLE_INPUT_INNER_HORIZONTAL_PADDING)));
checkBox = new JCheckBox();
checkBox.setEnabled(true);
@@ -1645,19 +1767,16 @@ public class XsdConfigDialog extends DialogWrapper {
checkBox.setRolloverEnabled(false);
checkBox.setBorderPainted(false);
checkBox.setMargin(new Insets(0, 0, 0, 0));
- checkBox.setPreferredSize(new Dimension(18, 18));
- checkBox.setMinimumSize(new Dimension(18, 18));
- checkBox.setMaximumSize(new Dimension(18, 18));
+ applyToggleCheckBoxSizing(checkBox);
textField = new JTextField();
textField.setEditable(false);
- textField.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
+ textField.setBorder(BorderFactory.createEmptyBorder(0, TOGGLE_INPUT_TEXT_HORIZONTAL_PADDING, 0, TOGGLE_INPUT_TEXT_HORIZONTAL_PADDING));
textField.setBackground(Palette.FIELD_BG);
textField.setForeground(Palette.TEXT_PRIMARY);
textField.setFont(textField.getFont().deriveFont(Font.PLAIN, 13f));
- textField.setPreferredSize(new Dimension(50, 20));
- textField.setMinimumSize(new Dimension(50, 20));
- textField.setMaximumSize(new Dimension(50, 20));
+ textField.setPreferredSize(new Dimension(TOGGLE_INPUT_TEXT_WIDTH, 20));
+ textField.setMinimumSize(new Dimension(TOGGLE_INPUT_TEXT_WIDTH, 20));
GridBagConstraints checkBoxConstraints = new GridBagConstraints();
checkBoxConstraints.gridx = 0;
@@ -1671,19 +1790,11 @@ public class XsdConfigDialog extends DialogWrapper {
textFieldConstraints.gridx = 1;
textFieldConstraints.gridy = 0;
textFieldConstraints.anchor = GridBagConstraints.WEST;
+ textFieldConstraints.weightx = 1.0;
textFieldConstraints.weighty = 1.0;
+ textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
textFieldConstraints.insets = new Insets(0, 0, 0, 0);
fieldPanel.add(textField, textFieldConstraints);
-
- JPanel filler = new JPanel();
- filler.setOpaque(false);
- GridBagConstraints fillerConstraints = new GridBagConstraints();
- fillerConstraints.gridx = 2;
- fillerConstraints.gridy = 0;
- fillerConstraints.weightx = 1.0;
- fillerConstraints.weighty = 1.0;
- fillerConstraints.fill = GridBagConstraints.HORIZONTAL;
- fieldPanel.add(filler, fillerConstraints);
add(fieldPanel, BorderLayout.CENTER);
}
@@ -1694,6 +1805,14 @@ public class XsdConfigDialog extends DialogWrapper {
boolean hasFocus,
int row,
int column) {
+ boolean showField = shouldRenderConfigField(table, row);
+ fieldPanel.setVisible(showField);
+ if (!showField) {
+ checkBox.setSelected(false);
+ textField.setText("");
+ setBackground(rowBackground(row, isSelected));
+ return this;
+ }
XsdTreeTableModel.ToggleInputValue toggleInputValue = value instanceof XsdTreeTableModel.ToggleInputValue
? (XsdTreeTableModel.ToggleInputValue) value
: XsdTreeTableModel.ToggleInputValue.EMPTY;
@@ -1715,14 +1834,14 @@ public class XsdConfigDialog extends DialogWrapper {
ToggleInputCellEditor() {
wrapper = new JPanel(new BorderLayout());
wrapper.setOpaque(true);
- wrapper.setBorder(new EmptyBorder(4, 8, 4, 8));
+ wrapper.setBorder(new EmptyBorder(4, TOGGLE_INPUT_OUTER_HORIZONTAL_PADDING, 4, TOGGLE_INPUT_OUTER_HORIZONTAL_PADDING));
fieldPanel = new JPanel(new GridBagLayout());
fieldPanel.setOpaque(true);
fieldPanel.setBackground(Palette.FIELD_BG);
fieldPanel.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(Palette.FIELD_BORDER, 1, true),
- new EmptyBorder(0, 6, 0, 6)));
+ new EmptyBorder(0, TOGGLE_INPUT_INNER_HORIZONTAL_PADDING, 0, TOGGLE_INPUT_INNER_HORIZONTAL_PADDING)));
checkBox = new JCheckBox();
checkBox.setOpaque(false);
@@ -1732,18 +1851,15 @@ public class XsdConfigDialog extends DialogWrapper {
checkBox.setRolloverEnabled(false);
checkBox.setBorderPainted(false);
checkBox.setMargin(new Insets(0, 0, 0, 0));
- checkBox.setPreferredSize(new Dimension(18, 18));
- checkBox.setMinimumSize(new Dimension(18, 18));
- checkBox.setMaximumSize(new Dimension(18, 18));
+ applyToggleCheckBoxSizing(checkBox);
textField = new JTextField();
- textField.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4));
+ textField.setBorder(BorderFactory.createEmptyBorder(0, TOGGLE_INPUT_TEXT_HORIZONTAL_PADDING, 0, TOGGLE_INPUT_TEXT_HORIZONTAL_PADDING));
textField.setBackground(Palette.FIELD_BG);
textField.setForeground(Palette.TEXT_PRIMARY);
textField.setFont(textField.getFont().deriveFont(Font.PLAIN, 13f));
- textField.setPreferredSize(new Dimension(50, 20));
- textField.setMinimumSize(new Dimension(50, 20));
- textField.setMaximumSize(new Dimension(50, 20));
+ textField.setPreferredSize(new Dimension(TOGGLE_INPUT_TEXT_WIDTH, 20));
+ textField.setMinimumSize(new Dimension(TOGGLE_INPUT_TEXT_WIDTH, 20));
GridBagConstraints checkBoxConstraints = new GridBagConstraints();
checkBoxConstraints.gridx = 0;
@@ -1757,19 +1873,11 @@ public class XsdConfigDialog extends DialogWrapper {
textFieldConstraints.gridx = 1;
textFieldConstraints.gridy = 0;
textFieldConstraints.anchor = GridBagConstraints.WEST;
+ textFieldConstraints.weightx = 1.0;
textFieldConstraints.weighty = 1.0;
+ textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
textFieldConstraints.insets = new Insets(0, 0, 0, 0);
fieldPanel.add(textField, textFieldConstraints);
-
- JPanel filler = new JPanel();
- filler.setOpaque(false);
- GridBagConstraints fillerConstraints = new GridBagConstraints();
- fillerConstraints.gridx = 2;
- fillerConstraints.gridy = 0;
- fillerConstraints.weightx = 1.0;
- fillerConstraints.weighty = 1.0;
- fillerConstraints.fill = GridBagConstraints.HORIZONTAL;
- fieldPanel.add(filler, fillerConstraints);
wrapper.add(fieldPanel, BorderLayout.CENTER);
installCellEditorKeyBindings(wrapper, this);
}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index d579201..d93ff83 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,7 +1,7 @@
com.dmiki.metaplugin
MetaPlugin
- 1.0.0
+ 1.1.0
RD5
\n"
+ "\n";
}
+
+ private String moduleRelativeMapping() {
+ return "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ }
+
+ private String groupOnlyMapping() {
+ return "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ }
+
+ private String nestedGroupMapping() {
+ return "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ }
+
+ private String flattenedFlatParentMapping() {
+ return "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ }
+
+ private String fullMappingWithFlattenedFlatParent() {
+ return "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ }
}
diff --git a/src/test/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolverTest.java b/src/test/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolverTest.java
new file mode 100644
index 0000000..cd83c20
--- /dev/null
+++ b/src/test/java/com/dmiki/metaplugin/xsd/ui/JavaPackageResolverTest.java
@@ -0,0 +1,54 @@
+package com.dmiki.metaplugin.xsd.ui;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class JavaPackageResolverTest {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void resolveDefaultPackage_prefersEntityThenDomainThenMessage() throws Exception {
+ File moduleDir = temporaryFolder.newFolder("module-entity-priority");
+ new File(moduleDir, "src/main/java/com/demo/message").mkdirs();
+ new File(moduleDir, "src/main/java/com/demo/domain").mkdirs();
+ new File(moduleDir, "src/main/java/com/demo/entity").mkdirs();
+
+ assertEquals("com.demo.entity", JavaPackageResolver.resolveDefaultPackage(moduleDir));
+ }
+
+ @Test
+ public void resolveDefaultPackage_infersCommonPackageWhenPreferredPackagesAreMissing() throws Exception {
+ File moduleDir = temporaryFolder.newFolder("module-common-package");
+ writeJavaFile(moduleDir, "src/main/java/com/acme/payment/service/PaymentService.java");
+ writeJavaFile(moduleDir, "src/main/java/com/acme/payment/model/PaymentModel.java");
+
+ assertEquals("com.acme.payment", JavaPackageResolver.resolveDefaultPackage(moduleDir));
+ }
+
+ @Test
+ public void resolveDefaultPackage_returnsNullWhenModuleIsNotJavaProject() throws Exception {
+ File moduleDir = temporaryFolder.newFolder("module-no-java");
+ new File(moduleDir, "src/main/resources").mkdirs();
+
+ assertNull(JavaPackageResolver.resolveDefaultPackage(moduleDir));
+ }
+
+ private void writeJavaFile(File moduleDir, String relativePath) throws Exception {
+ File javaFile = new File(moduleDir, relativePath);
+ File parent = javaFile.getParentFile();
+ if (parent != null) {
+ parent.mkdirs();
+ }
+ Files.write(javaFile.toPath(), "class Test {}".getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/src/test/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialogTest.java b/src/test/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialogTest.java
new file mode 100644
index 0000000..ce67609
--- /dev/null
+++ b/src/test/java/com/dmiki/metaplugin/xsd/ui/XsdConfigDialogTest.java
@@ -0,0 +1,37 @@
+package com.dmiki.metaplugin.xsd.ui;
+
+import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
+import com.intellij.ui.treeStructure.treetable.TreeTable;
+import org.junit.Test;
+
+import java.awt.Dimension;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class XsdConfigDialogTest {
+
+ @Test
+ public void shouldRenderConfigField_hidesRendererShellForRootRow() {
+ XsdConfigItem root = new XsdConfigItem("Document", false, false, false);
+ root.getChildren().add(new XsdConfigItem("GrpHdr", true, false, false));
+
+ DefaultMutableTreeNode swingRoot = XsdTreeTableModel.toSwingTree(root);
+ TreeTable table = new TreeTable(new XsdTreeTableModel(swingRoot));
+ table.getTree().setRootVisible(true);
+ table.getTree().expandRow(0);
+
+ assertFalse(XsdConfigDialog.shouldRenderConfigField(table, 0));
+ assertTrue(XsdConfigDialog.shouldRenderConfigField(table, 1));
+ }
+
+ @Test
+ public void toggleCheckBoxSize_usesLargestAxisAndMinimumFloor() {
+ assertEquals(new Dimension(20, 20), XsdConfigDialog.toggleCheckBoxSize(new Dimension(18, 18)));
+ assertEquals(new Dimension(22, 22), XsdConfigDialog.toggleCheckBoxSize(new Dimension(22, 18)));
+ assertEquals(new Dimension(20, 20), XsdConfigDialog.toggleCheckBoxSize(null));
+ }
+}