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)); + } +}