This commit is contained in:
dlandy
2026-04-15 16:04:29 +08:00
parent 188b497e15
commit 9f66afe148
17 changed files with 1552 additions and 237 deletions

View File

@@ -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

View File

@@ -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`

218
docs/hvps.101.001.01.xsd Normal file
View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns="urn:cnaps:std:hvps:2010:tech:xsd:hvps.111.001.01"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:cnaps:std:hvps:2010:tech:xsd:hvps.111.001.01"
elementFormDefault="qualified">
<!-- 根节点 Document -->
<xs:element name="Document" type="Document"/>
<!-- 1. 文档结构 -->
<xs:complexType name="Document">
<xs:sequence>
<xs:element name="FIToFICstmrCdtTrf" type="FIToFICstmrCdtTrf"/>
</xs:sequence>
</xs:complexType>
<!-- 2. 金融机构间客户汇兑主体 (大额汇兑) -->
<xs:complexType name="FIToFICstmrCdtTrf">
<xs:sequence>
<!-- 业务公共报头 -->
<xs:element name="GrpHdr" type="GroupHeader"/>
<!-- 资金交易明细 (大额报文通常只有一笔明细) -->
<xs:element name="CdtTrfTxInf" type="CreditTransferTransactionInformation" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- 3. 业务头 (Group Header) -->
<xs:complexType name="GroupHeader">
<xs:sequence>
<xs:element name="MsgId" type="Max35Text"/> <!-- 报文标识号 -->
<xs:element name="CreDtTm" type="ISODateTime"/> <!-- 报文创建时间 -->
<xs:element name="InstgPty" type="InstructingParty"/> <!-- 发起参与机构 -->
<xs:element name="InstdPty" type="InstructedParty"/> <!-- 接收参与机构 -->
<xs:element name="SysCd" type="SystemCode"/> <!-- 系统代码 (如 HVPS) -->
<xs:element name="Rmk" type="Max256Text" minOccurs="0"/> <!-- 备注 -->
</xs:sequence>
</xs:complexType>
<!-- 4. 汇兑交易信息 (Transaction Information) -->
<xs:complexType name="CreditTransferTransactionInformation">
<xs:sequence>
<xs:element name="PmtId" type="PaymentIdentification"/> <!-- 支付标识 -->
<xs:element name="PmtTpInf" type="PaymentTypeInformation" minOccurs="0"/> <!-- 支付类型信息 (业务种类) -->
<xs:element name="IntrBkSttlmAmt" type="ActiveCurrencyAndAmount"/> <!-- 银行间结算金额 -->
<xs:element name="Dbtr" type="PartyIdentification"/> <!-- 付款人 -->
<xs:element name="DbtrAcct" type="CashAccount" minOccurs="0"/> <!-- 付款人账户 -->
<xs:element name="DbtrAgt" type="BranchAndFinancialInstitutionIdentification"/> <!-- 付款行 -->
<xs:element name="CdtrAgt" type="BranchAndFinancialInstitutionIdentification"/> <!-- 收款行 -->
<xs:element name="Cdtr" type="PartyIdentification"/> <!-- 收款人 -->
<xs:element name="CdtrAcct" type="CashAccount"/> <!-- 收款人账户 -->
<xs:element name="Purp" type="Purpose" minOccurs="0"/> <!-- 用途/附言 -->
</xs:sequence>
</xs:complexType>
<!-- ==== 以下为子组件装配定义 ==== -->
<xs:complexType name="PaymentIdentification">
<xs:sequence>
<xs:element name="EndToEndId" type="Max35Text"/> <!-- 端到端标识号 -->
<xs:element name="TxId" type="Max35Text"/> <!-- 交易标识号 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="InstructingParty">
<xs:sequence>
<xs:element name="InstgDrctPty" type="Max14Text"/> <!-- 发起直接参与机构 (14位行号) -->
<xs:element name="InstgPty" type="Max14Text" minOccurs="0"/> <!-- 发起间接参与机构 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="InstructedParty">
<xs:sequence>
<xs:element name="InstdDrctPty" type="Max14Text"/> <!-- 接收直接参与机构 (14位行号) -->
<xs:element name="InstdPty" type="Max14Text" minOccurs="0"/> <!-- 接收间接参与机构 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="PartyIdentification">
<xs:sequence>
<xs:element name="Nm" type="Max60Text"/> <!-- 户名 -->
<xs:element name="PstlAdr" type="PostalAddress" minOccurs="0"/> <!-- 地址 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="CashAccount">
<xs:sequence>
<xs:element name="Id" type="AccountIdentification"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="AccountIdentification">
<xs:sequence>
<xs:element name="Othr" type="GenericAccountIdentification"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="GenericAccountIdentification">
<xs:sequence>
<xs:element name="Id" type="Max32Text"/> <!-- 账号 (一般最长32位) -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="BranchAndFinancialInstitutionIdentification">
<xs:sequence>
<xs:element name="FinInstnId" type="FinancialInstitutionIdentification"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="FinancialInstitutionIdentification">
<xs:sequence>
<xs:element name="ClrSysMmbId" type="ClearingSystemMemberIdentification"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ClearingSystemMemberIdentification">
<xs:sequence>
<xs:element name="MmbId" type="Max14Text"/> <!-- CNAPS 14位支付行号 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="PaymentTypeInformation">
<xs:sequence>
<xs:element name="CtgyPurp" type="CategoryPurpose"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="CategoryPurpose">
<xs:sequence>
<xs:element name="Prtry" type="Max5Text"/> <!-- 二代支付中业务种类为最多5位数字如大额A100 -->
</xs:sequence>
</xs:complexType>
<xs:complexType name="Purpose">
<xs:sequence>
<xs:element name="Prtry" type="Max256Text"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="PostalAddress">
<xs:sequence>
<xs:element name="AdrLine" type="Max256Text" minOccurs="0" maxOccurs="2"/>
</xs:sequence>
</xs:complexType>
<!-- ==== 以下为基本数据类型 (Simple Types) ==== -->
<xs:simpleType name="Max35Text">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:maxLength value="35"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Max32Text">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:maxLength value="32"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Max14Text">
<xs:restriction base="xs:string">
<xs:length value="14"/> <!-- 人行行号通常定长14位 -->
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Max5Text">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:maxLength value="5"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Max60Text">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:maxLength value="60"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Max256Text">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
<xs:maxLength value="256"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="SystemCode">
<xs:restriction base="xs:string">
<xs:enumeration value="HVPS"/> <!-- 单笔大额 -->
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ISODateTime">
<xs:restriction base="xs:dateTime"/>
</xs:simpleType>
<!-- 人民币金额类型定义 -->
<xs:complexType name="ActiveCurrencyAndAmount">
<xs:simpleContent>
<xs:extension base="ActiveCurrencyAndAmount_SimpleType">
<xs:attribute name="Ccy" type="ActiveCurrencyCode" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="ActiveCurrencyAndAmount_SimpleType">
<xs:restriction base="xs:decimal">
<xs:fractionDigits value="2"/>
<xs:totalDigits value="18"/>
<xs:minInclusive value="0.01"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ActiveCurrencyCode">
<xs:restriction base="xs:string">
<xs:pattern value="[A-Z]{3,3}"/> <!-- 绝大多数情况为 CNY -->
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -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 resolveResourcesDirectory(XomGenerateOptions options) throws XomGenerationException {
String projectBasePath = trimToNull(options.getProjectBasePath());
if (projectBasePath != null) {
return new File(projectBasePath, "src/main/resources");
private File resolveOutputFile(String outputPath, File moduleBaseDir) {
return XomPathStrategy.resolvePath(outputPath, moduleBaseDir);
}
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) {

View File

@@ -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;
}
}

View File

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

View File

@@ -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<XsdConfigItem> 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<ResultNode> topResults) {
if (root == null) {
return;
}
List<ResultNode> pool = new ArrayList<ResultNode>(topResults);
ResultNode direct = removeFirstMatch(pool, root);
ResultNode direct = removeFirstDirectMatch(pool, root);
if (direct != null) {
applyDirectConfig(root, direct);
applyChildrenReplay(root, new ArrayList<ResultNode>(direct.getChildren()));
@@ -114,7 +144,7 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
private void applyChildrenReplay(XsdConfigItem parent, List<ResultNode> 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<ResultNode>(direct.getChildren()));
@@ -156,10 +186,10 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
return false;
}
private ResultNode removeFirstMatch(List<ResultNode> pool, XsdConfigItem item) {
private ResultNode removeFirstDirectMatch(List<ResultNode> 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<ResultNode> resolveReplayPool(List<ResultNode> topResults, List<XsdConfigItem> targetPath) {
List<XsdConfigItem> ancestors = new ArrayList<XsdConfigItem>();
if (targetPath != null && targetPath.size() > 1) {
ancestors.addAll(targetPath.subList(0, targetPath.size() - 1));
}
List<ReplayPool> pools = new ArrayList<ReplayPool>();
collectReplayPools(topResults, new ArrayList<ResultNode>(), 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<ResultNode>();
}
return new ArrayList<ResultNode>(best.getNodes());
}
private void collectReplayPools(List<ResultNode> nodes, List<ResultNode> ancestors, List<ReplayPool> pools) {
pools.add(new ReplayPool(nodes, ancestors));
if (nodes == null || nodes.isEmpty()) {
return;
}
for (ResultNode node : nodes) {
List<ResultNode> nextAncestors = new ArrayList<ResultNode>(ancestors);
nextAncestors.add(node);
collectReplayPools(node.getChildren(), nextAncestors, pools);
}
}
private int matchAncestorSuffix(List<XsdConfigItem> ancestors, List<ResultNode> 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<XsdConfigItem> resolvePath(XsdConfigItem root, XsdConfigItem targetNode) {
List<XsdConfigItem> path = new ArrayList<XsdConfigItem>();
return collectPath(root, targetNode, path) ? path : null;
}
private boolean collectPath(XsdConfigItem current, XsdConfigItem target, List<XsdConfigItem> 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<File> candidates = new ArrayList<File>();
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<String> dedup = new LinkedHashSet<String>();
@@ -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<ResultNode> parseResultChildren(Element parent,
Set<String> customAttributeValues,
Set<String> preProcessorValues) {
@@ -482,4 +598,22 @@ public class ReservedXomReverseDisplayService implements XomReverseDisplayServic
return children;
}
}
private static final class ReplayPool {
private final List<ResultNode> nodes;
private final List<ResultNode> ancestorPath;
private ReplayPool(List<ResultNode> nodes, List<ResultNode> ancestorPath) {
this.nodes = nodes == null ? new ArrayList<ResultNode>() : new ArrayList<ResultNode>(nodes);
this.ancestorPath = ancestorPath == null ? new ArrayList<ResultNode>() : new ArrayList<ResultNode>(ancestorPath);
}
private List<ResultNode> getNodes() {
return nodes;
}
private List<ResultNode> getAncestorPath() {
return ancestorPath;
}
}
}

View File

@@ -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;
}

View File

@@ -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<File> sourceRoots = collectJavaSourceRoots(moduleBaseDir);
if (sourceRoots.isEmpty()) {
return null;
}
String preferredPackage = resolvePreferredPackage(sourceRoots);
if (preferredPackage != null) {
return preferredPackage;
}
return inferModulePackage(sourceRoots);
}
static List<File> collectJavaSourceRoots(File moduleBaseDir) {
if (moduleBaseDir == null || !moduleBaseDir.isDirectory()) {
return Collections.emptyList();
}
Map<String, File> sourceRoots = new LinkedHashMap<String, File>();
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<File>(sourceRoots.values());
}
private static void registerSourceRoot(Map<String, File> 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<File> sourceRoots) {
for (String preferredPackageName : PREFERRED_PACKAGES) {
List<String> candidates = new ArrayList<String>();
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<String> candidates) {
if (sourceRoot == null || directoryName == null) {
return;
}
Deque<File> stack = new ArrayDeque<File>();
stack.push(sourceRoot);
while (!stack.isEmpty()) {
File current = stack.pop();
List<File> 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<File> sourceRoots) {
Set<String> packageNames = new LinkedHashSet<String>();
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<String>(packageNames));
}
private static void collectJavaFilePackages(File sourceRoot, Set<String> packageNames) {
Deque<File> stack = new ArrayDeque<File>();
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<String> segments = new ArrayList<String>();
File current = sourceRoot;
while (current != null && current.isDirectory()) {
if (containsJavaFiles(current)) {
break;
}
List<File> 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<String> 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<String> packageNames) {
if (packageNames == null || packageNames.isEmpty()) {
return null;
}
Collections.sort(packageNames, new Comparator<String>() {
@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<File> listDirectories(File directory) {
List<File> directories = new ArrayList<File>();
for (File child : listChildren(directory)) {
if (child.isDirectory() && !isSkippableDirectory(child)) {
directories.add(child);
}
}
Collections.sort(directories, new Comparator<File>() {
@Override
public int compare(File left, File right) {
return left.getName().compareTo(right.getName());
}
});
return directories;
}
private static List<File> listPackageDirectories(File directory) {
List<File> directories = new ArrayList<File>();
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<String> segments) {
StringBuilder builder = new StringBuilder();
for (String segment : segments) {
if (builder.length() > 0) {
builder.append('.');
}
builder.append(segment);
}
return builder.toString();
}
}

View File

@@ -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, '/');
return XomPathStrategy.toRelativePath(moduleBaseDir, file);
}
private File moduleBaseDir() {
String basePath = trimToNull(currentModuleBasePath);
return basePath == null ? null : new File(basePath);
}
} catch (Exception ignore) {
return file.getAbsolutePath();
private String resolveModuleBasePath(File file) {
File moduleBaseDir = resolveModuleBaseDir(file);
return moduleBaseDir == null ? null : moduleBaseDir.getAbsolutePath();
}
return file.getAbsolutePath();
private File resolveModuleBaseDir(File file) {
File contentRoot = findContentRoot(file);
if (contentRoot != null) {
return contentRoot;
}
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);
}

View File

@@ -1,7 +1,7 @@
<idea-plugin>
<id>com.dmiki.metaplugin</id>
<name>MetaPlugin</name>
<version>1.0.0</version>
<version>1.1.0</version>
<vendor email="support@example.com" url="https://example.com">RD5</vendor>
<description><![CDATA[

View File

@@ -51,7 +51,9 @@
### 目标
将 XSD 结构转为“可执行的自定义 XML 映射配置”,用于后续 生成Java 实体与 XML 的双向/单向序列化流程。
1、支持生成xml映射文件默认存放在resources下msgmapper目录下如果msgmapper存在则放在msgmapper1下如果msgmapper1存在则放在msgmapper2下以此类推。
1、支持生成xml映射文件
- 如果XSD位于某个resources目录下则默认存放在该resources下的msgmapper目录如果msgmapper存在则放在msgmapper1下如果msgmapper1存在则放在msgmapper2下以此类推。
- 如果XSD不在resources目录下则默认存放在XSD父级目录的同级msgmapper目录命名冲突时同样按msgmapper1、msgmapper2递增。
2、支持生成java实体类与上一步的xml文件在同一个目录
3、预留后续根据xml映射文件和xsd反向展示能里

View File

@@ -80,6 +80,70 @@ public class XomGenerationServiceTest {
assertTrue(expected.isFile());
}
@Test
public void generate_defaultsToResourcesMsgMapperWhenXsdIsUnderResources() throws Exception {
File moduleBase = Files.createTempDirectory("xom-service-module-resources").toFile();
File xsdFile = new File(moduleBase, "src/main/resources/xsd/demo.xsd");
File existingMsgmapper = new File(moduleBase, "src/main/resources/msgmapper");
assertTrue(xsdFile.getParentFile().mkdirs() || xsdFile.getParentFile().isDirectory());
assertTrue(existingMsgmapper.mkdirs() || existingMsgmapper.isDirectory());
assertTrue(xsdFile.createNewFile() || xsdFile.isFile());
XomGenerateOptions options = new XomGenerateOptions(
moduleBase.getAbsolutePath(),
"message",
"demo",
null,
xsdFile.getAbsolutePath(),
null,
false,
true,
true,
false,
false,
false,
8
);
XomGenerationResult result = service.generate(simpleRoot(), options);
File expected = new File(moduleBase, "src/main/resources/msgmapper1/demo.xml");
assertEquals(expected.getCanonicalFile(), result.getMappingXmlFile().getCanonicalFile());
assertTrue(expected.isFile());
}
@Test
public void generate_defaultsToSiblingMsgMapperWhenXsdIsOutsideResources() throws Exception {
File moduleBase = Files.createTempDirectory("xom-service-module-sibling").toFile();
File xsdFile = new File(moduleBase, "schema/inbound/demo.xsd");
File existingMsgmapper = new File(moduleBase, "schema/msgmapper");
assertTrue(xsdFile.getParentFile().mkdirs() || xsdFile.getParentFile().isDirectory());
assertTrue(existingMsgmapper.mkdirs() || existingMsgmapper.isDirectory());
assertTrue(xsdFile.createNewFile() || xsdFile.isFile());
XomGenerateOptions options = new XomGenerateOptions(
moduleBase.getAbsolutePath(),
"message",
"demo",
null,
xsdFile.getAbsolutePath(),
null,
false,
true,
true,
false,
false,
false,
8
);
XomGenerationResult result = service.generate(simpleRoot(), options);
File expected = new File(moduleBase, "schema/msgmapper1/demo.xml");
assertEquals(expected.getCanonicalFile(), result.getMappingXmlFile().getCanonicalFile());
assertTrue(expected.isFile());
}
private XomGenerateOptions options(File xsdFile, File outputFile, String resultMapType, boolean strictMode) {
return new XomGenerateOptions(
null,

View File

@@ -106,10 +106,11 @@ public class XomXmlRendererTest {
}
@Test
public void render_schemaUsesResourceRelativeXsdPathWhenProjectBaseProvided() throws Exception {
public void render_schemaUsesModuleRelativeXsdPathWhenXsdIsOutsideResources() throws Exception {
File projectBase = Files.createTempDirectory("xom-render-project").toFile();
File xsdFile = new File(projectBase, "xsd/camt.053.001.01.xsd");
xsdFile.getParentFile().mkdirs();
assertTrue(xsdFile.createNewFile() || xsdFile.isFile());
XsdConfigItem root = new XsdConfigItem("Root", false, false, false);
root.setJavaProperty("root");
@@ -130,8 +131,7 @@ public class XomXmlRendererTest {
8
);
String xml = renderer.render(root, options);
Path resourcePath = projectBase.toPath().resolve("src/main/resources");
String expectedRelative = resourcePath.relativize(xsdFile.toPath())
String expectedRelative = projectBase.toPath().relativize(xsdFile.toPath())
.toString()
.replace(File.separatorChar, '/');
@@ -139,6 +139,40 @@ public class XomXmlRendererTest {
assertFalse(xml.contains("reverse-reserved"));
}
@Test
public void render_schemaUsesResourcesRelativeXsdPathWhenXsdIsUnderResources() throws Exception {
File projectBase = Files.createTempDirectory("xom-render-project-resources").toFile();
File xsdFile = new File(projectBase, "src/main/resources/xsd/demo.xsd");
xsdFile.getParentFile().mkdirs();
assertTrue(xsdFile.createNewFile() || xsdFile.isFile());
XsdConfigItem root = new XsdConfigItem("Root", false, false, false);
root.setJavaProperty("root");
XomGenerateOptions options = new XomGenerateOptions(
projectBase.getAbsolutePath(),
"message",
"schema",
"com.demo.Root",
xsdFile.getAbsolutePath(),
null,
false,
true,
true,
false,
false,
false,
8
);
String xml = renderer.render(root, options);
Path resourcesPath = projectBase.toPath().resolve("src/main/resources");
String expectedRelative = resourcesPath.relativize(xsdFile.toPath())
.toString()
.replace(File.separatorChar, '/');
assertTrue(xml.contains("xsdPath=\"" + expectedRelative + "\""));
}
@Test
public void render_rendersAttributeCarrierNodeWithAttributeChild() {
XsdConfigItem root = new XsdConfigItem("Document", false, false, false);

View File

@@ -2,6 +2,7 @@ package com.dmiki.metaplugin.xom.reverse;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import com.dmiki.metaplugin.xsd.parser.XsdSchemaParser;
import org.junit.Test;
import java.io.File;
@@ -16,6 +17,7 @@ import static org.junit.Assert.fail;
public class ReservedXomReverseDisplayServiceTest {
private final ReservedXomReverseDisplayService service = new ReservedXomReverseDisplayService();
private final XsdSchemaParser parser = new XsdSchemaParser();
@Test
public void loadForDisplay_replaysNodeConfigAndTopFields() throws Exception {
@@ -112,6 +114,140 @@ public class ReservedXomReverseDisplayServiceTest {
assertEquals("id", id.getJavaProperty());
}
@Test
public void loadForDisplay_resolvesModuleRelativeXsdPathOutsideResources() throws Exception {
File moduleDir = Files.createTempDirectory("reverse-project-module-relative").toFile();
File xsdFile = new File(moduleDir, "schema/inbound/demo.xsd");
File xomFile = new File(moduleDir, "schema/msgmapper/demo.xml");
writeFile(xsdFile, simpleSchema());
writeFile(xomFile, moduleRelativeMapping());
XomReverseDisplayResult result = service.loadForDisplay(xomFile, moduleDir.getAbsolutePath());
assertEquals(xsdFile.getAbsolutePath(), result.getXsdFile().getAbsolutePath());
XsdConfigItem root = result.getRoot();
XsdConfigItem id = childByName(root, "Id");
assertNotNull(id);
assertEquals(XsdConfigMode.KEEP, id.getConfigMode());
assertEquals("id", id.getJavaProperty());
}
@Test
public void applyToCurrentTree_replaysSelectedSubtreeFromPartialMapping() throws Exception {
File projectDir = Files.createTempDirectory("reverse-apply-subtree").toFile();
File resourcesDir = new File(projectDir, "src/main/resources");
File xsdFile = new File(resourcesDir, "xsd/demo.xsd");
File xomFile = new File(resourcesDir, "msgmapper/group.xml");
writeFile(xsdFile, schemaForReplay());
writeFile(xomFile, groupOnlyMapping());
XsdConfigItem currentRoot = parser.parse(xsdFile);
XsdConfigItem group = childByName(currentRoot, "Group");
XsdConfigItem msgId = childByName(group, "MsgId");
XsdConfigItem creDtTm = childByName(group, "CreDtTm");
group.setJavaProperty("staleGroup");
creDtTm.setJavaProperty("staleCreDtTm");
creDtTm.setConfigMode(XsdConfigMode.KEEP);
XomReverseDisplayResult result = service.applyToCurrentTree(
xomFile,
projectDir.getAbsolutePath(),
currentRoot,
group);
assertEquals(xsdFile.getAbsolutePath(), result.getXsdFile().getAbsolutePath());
assertEquals(XsdConfigMode.PRESERVE_HIERARCHY, group.getConfigMode());
assertNull(group.getJavaProperty());
assertEquals(XsdConfigMode.KEEP, msgId.getConfigMode());
assertEquals("msgId", msgId.getJavaProperty());
assertTrue(msgId.isCustomAttributeEnabled());
assertTrue(msgId.isPreProcessorEnabled());
assertEquals(XsdConfigMode.IGNORE, creDtTm.getConfigMode());
assertNull(creDtTm.getJavaProperty());
}
@Test
public void applyToCurrentTree_usesClosestAncestorPoolForNestedSelection() throws Exception {
File projectDir = Files.createTempDirectory("reverse-apply-nested").toFile();
File resourcesDir = new File(projectDir, "src/main/resources");
File xsdFile = new File(resourcesDir, "xsd/demo.xsd");
File xomFile = new File(resourcesDir, "msgmapper/group-nested.xml");
writeFile(xsdFile, schemaForReplay());
writeFile(xomFile, nestedGroupMapping());
XsdConfigItem currentRoot = parser.parse(xsdFile);
XsdConfigItem group = childByName(currentRoot, "Group");
XsdConfigItem msgId = childByName(group, "MsgId");
msgId.setJavaProperty("staleMsgId");
msgId.setConfigMode(XsdConfigMode.IGNORE);
service.applyToCurrentTree(xomFile, projectDir.getAbsolutePath(), currentRoot, msgId);
assertEquals(XsdConfigMode.KEEP, msgId.getConfigMode());
assertEquals("msgId", msgId.getJavaProperty());
}
@Test
public void applyToCurrentTree_supportsFlattenedLabelsGeneratedForSelectedNode() throws Exception {
File projectDir = Files.createTempDirectory("reverse-apply-flatten").toFile();
File resourcesDir = new File(projectDir, "src/main/resources");
File xsdFile = new File(resourcesDir, "xsd/demo.xsd");
File xomFile = new File(resourcesDir, "msgmapper/flat-parent.xml");
writeFile(xsdFile, schemaForReplay());
writeFile(xomFile, flattenedFlatParentMapping());
XsdConfigItem currentRoot = parser.parse(xsdFile);
XsdConfigItem flatParent = childByName(currentRoot, "FlatParent");
XsdConfigItem a = childByName(flatParent, "A");
XsdConfigItem b = childByName(flatParent, "B");
a.setJavaProperty("staleA");
b.setJavaProperty("staleB");
service.applyToCurrentTree(xomFile, projectDir.getAbsolutePath(), currentRoot, flatParent);
assertEquals(XsdConfigMode.FLATTEN, flatParent.getConfigMode());
assertEquals(XsdConfigMode.KEEP, a.getConfigMode());
assertEquals("a", a.getJavaProperty());
assertEquals(XsdConfigMode.KEEP, b.getConfigMode());
assertEquals("b", b.getJavaProperty());
}
@Test
public void applyToCurrentTree_preservesFlattenModeForNestedSelectedNodeInFullMapping() throws Exception {
File projectDir = Files.createTempDirectory("reverse-apply-nested-flatten").toFile();
File resourcesDir = new File(projectDir, "src/main/resources");
File xsdFile = new File(resourcesDir, "xsd/demo.xsd");
File xomFile = new File(resourcesDir, "msgmapper/nested-flat-parent.xml");
writeFile(xsdFile, schemaForReplay());
writeFile(xomFile, fullMappingWithFlattenedFlatParent());
XsdConfigItem currentRoot = parser.parse(xsdFile);
XsdConfigItem flatParent = childByName(currentRoot, "FlatParent");
XsdConfigItem a = childByName(flatParent, "A");
XsdConfigItem b = childByName(flatParent, "B");
flatParent.setJavaProperty("staleFlatParent");
flatParent.setConfigMode(XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES);
a.setJavaProperty("staleA");
a.setConfigMode(XsdConfigMode.IGNORE);
b.setJavaProperty("staleB");
b.setConfigMode(XsdConfigMode.IGNORE);
service.applyToCurrentTree(xomFile, projectDir.getAbsolutePath(), currentRoot, flatParent);
assertEquals(XsdConfigMode.FLATTEN, flatParent.getConfigMode());
assertNull(flatParent.getJavaProperty());
assertEquals(XsdConfigMode.KEEP, a.getConfigMode());
assertEquals("a", a.getJavaProperty());
assertEquals(XsdConfigMode.KEEP, b.getConfigMode());
assertEquals("b", b.getJavaProperty());
}
private void writeFile(File file, String content) throws Exception {
File parent = file.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
@@ -223,4 +359,70 @@ public class ReservedXomReverseDisplayServiceTest {
+ " </resultMap>\n"
+ "</message>\n";
}
private String moduleRelativeMapping() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<message>\n"
+ " <schema id=\"demo\" xsdPath=\"schema/inbound/demo.xsd\"/>\n"
+ " <resultMap id=\"demo\" type=\"com.demo.Document\">\n"
+ " <result label=\"Document\">\n"
+ " <result property=\"id\" label=\"Id\"/>\n"
+ " </result>\n"
+ " </resultMap>\n"
+ "</message>\n";
}
private String groupOnlyMapping() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<message>\n"
+ " <schema id=\"demo\" xsdPath=\"xsd/demo.xsd\"/>\n"
+ " <resultMap id=\"demo\" type=\"com.demo.Document\">\n"
+ " <result lable=\"Group\">\n"
+ " <result property=\"msgId\" lable=\"MsgId\" attribute=\"sign\" pre-processor=\"trim\"/>\n"
+ " </result>\n"
+ " </resultMap>\n"
+ "</message>\n";
}
private String nestedGroupMapping() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<message>\n"
+ " <schema id=\"demo\" xsdPath=\"xsd/demo.xsd\"/>\n"
+ " <resultMap id=\"demo\" type=\"com.demo.Document\">\n"
+ " <result lable=\"Group\">\n"
+ " <result property=\"msgId\" lable=\"MsgId\"/>\n"
+ " </result>\n"
+ " </resultMap>\n"
+ "</message>\n";
}
private String flattenedFlatParentMapping() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<message>\n"
+ " <schema id=\"demo\" xsdPath=\"xsd/demo.xsd\"/>\n"
+ " <resultMap id=\"demo\" type=\"com.demo.Document\">\n"
+ " <result property=\"a\" lable=\"FlatParent:A\"/>\n"
+ " <result property=\"b\" lable=\"FlatParent:B\"/>\n"
+ " </resultMap>\n"
+ "</message>\n";
}
private String fullMappingWithFlattenedFlatParent() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<message>\n"
+ " <schema id=\"demo\" xsdPath=\"xsd/demo.xsd\"/>\n"
+ " <resultMap id=\"demo\" type=\"com.demo.Document\">\n"
+ " <result property=\"document\" lable=\"Document\">\n"
+ " <result label=\"Group\">\n"
+ " <result property=\"msgId\" label=\"MsgId\"/>\n"
+ " </result>\n"
+ " <result property=\"a\" lable=\"FlatParent:A\"/>\n"
+ " <result property=\"b\" lable=\"FlatParent:B\"/>\n"
+ " <result property=\"amt\" label=\"Amt\">\n"
+ " <result property=\"ccy\" label-attribute=\"Ccy\"/>\n"
+ " </result>\n"
+ " </result>\n"
+ " </resultMap>\n"
+ "</message>\n";
}
}

View File

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

View File

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