This commit is contained in:
dlandy
2026-03-27 10:02:59 +08:00
commit 188b497e15
204 changed files with 17633 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
package com.dmiki.metaplugin.action;
import com.dmiki.metaplugin.xsd.ui.XsdConfigDialog;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.Locale;
public class XsdStructureConfigAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
VirtualFile selectedFile = event.getData(CommonDataKeys.VIRTUAL_FILE);
File xsdFile = toXsdFile(selectedFile);
File xomFile = toXomFile(selectedFile);
XsdConfigDialog dialog;
if (xsdFile != null) {
dialog = new XsdConfigDialog(event.getProject(), xsdFile, true);
} else if (xomFile != null) {
dialog = new XsdConfigDialog(event.getProject(), null, xomFile, true);
} else {
dialog = new XsdConfigDialog(event.getProject());
}
dialog.show();
}
@Override
public void update(@NotNull AnActionEvent event) {
Presentation presentation = event.getPresentation();
String place = event.getPlace();
boolean popupPlace = ActionPlaces.PROJECT_VIEW_POPUP.equals(place) || ActionPlaces.EDITOR_POPUP.equals(place);
VirtualFile selectedFile = event.getData(CommonDataKeys.VIRTUAL_FILE);
boolean visible = popupPlace && (toXsdFile(selectedFile) != null || toXomFile(selectedFile) != null);
presentation.setVisible(visible);
presentation.setEnabled(visible);
presentation.setText("XOM生成");
}
private File toXsdFile(VirtualFile file) {
if (file == null || file.isDirectory()) {
return null;
}
String extension = file.getExtension();
if (extension == null || !"xsd".equalsIgnoreCase(extension)) {
return null;
}
return new File(file.getPath());
}
private File toXomFile(VirtualFile file) {
if (file == null || file.isDirectory()) {
return null;
}
String extension = file.getExtension();
if (extension == null) {
return null;
}
String normalized = extension.toLowerCase(Locale.ROOT);
if (!"xml".equals(normalized) && !"xom".equals(normalized)) {
return null;
}
return new File(file.getPath());
}
}

View File

@@ -0,0 +1,648 @@
package com.dmiki.metaplugin.xom;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class JavaEntityGenerator {
public File generate(XsdConfigItem root,
XomGenerateOptions options,
File outputDirectory) throws XomGenerationException {
if (root == null) {
throw new XomGenerationException("XOM-004", "实体生成失败: 根节点为空");
}
if (outputDirectory == null) {
throw new XomGenerationException("XOM-006", "实体生成失败: 输出目录为空");
}
if (!outputDirectory.exists() && !outputDirectory.mkdirs()) {
throw new XomGenerationException("XOM-006", "实体生成失败: 无法创建目录 " + outputDirectory.getAbsolutePath());
}
JavaTypeDescriptor rootType = parseRootType(options, root);
GenerationContext context = new GenerationContext();
String rootClassName = ensureUniqueClassName(rootType.className, context.classCounters);
List<ClassModel> classModels = new ArrayList<ClassModel>();
ClassModel rootModel = new ClassModel(rootClassName);
classModels.add(rootModel);
buildClassModel(root, rootModel, classModels, context);
List<File> targetFiles = new ArrayList<File>();
for (ClassModel model : classModels) {
File file = new File(outputDirectory, model.className + ".java");
if (file.exists() && !options.isOverwrite()) {
throw new XomGenerationException("XOM-006", "Java文件已存在且overwrite=false: " + file.getAbsolutePath());
}
targetFiles.add(file);
}
for (int i = 0; i < classModels.size(); i++) {
String source = renderSource(classModels.get(i), rootType.packageName);
writeSource(targetFiles.get(i), source);
}
return targetFiles.get(0);
}
private JavaTypeDescriptor parseRootType(XomGenerateOptions options, XsdConfigItem root) {
String fullType = trimToNull(options.getResultMapType());
if (fullType == null) {
return new JavaTypeDescriptor(null, toUpperCamel(safe(root.getXmlName())) + "Entity");
}
int idx = fullType.lastIndexOf('.');
if (idx < 0 || idx == fullType.length() - 1) {
return new JavaTypeDescriptor(null, sanitizeDeclaredClassName(fullType));
}
String packageName = fullType.substring(0, idx).trim();
String className = sanitizeDeclaredClassName(fullType.substring(idx + 1));
return new JavaTypeDescriptor(packageName.isEmpty() ? null : packageName, className);
}
private void buildClassModel(XsdConfigItem node,
ClassModel classModel,
List<ClassModel> allClasses,
GenerationContext context) {
Map<String, Integer> fieldCounters = new LinkedHashMap<String, Integer>();
appendSelfValueFieldIfNeeded(node, classModel, fieldCounters);
for (XsdConfigItem child : node.getChildren()) {
appendNodeAsField(classModel, child, fieldCounters, context, allClasses);
}
}
private void appendSelfValueFieldIfNeeded(XsdConfigItem node,
ClassModel classModel,
Map<String, Integer> fieldCounters) {
if (!shouldAppendSelfValueField(node)) {
return;
}
String fieldName = ensureUniqueField(resolveFieldName(node), fieldCounters);
String fieldType = resolveLeafType(node.getJavaType());
classModel.fields.add(new FieldModel(fieldName, fieldType));
}
private boolean shouldAppendSelfValueField(XsdConfigItem node) {
return isSimpleContentNode(node);
}
private void appendNodeAsField(ClassModel classModel,
XsdConfigItem item,
Map<String, Integer> fieldCounters,
GenerationContext context,
List<ClassModel> allClasses) {
boolean leaf = item.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(item, leaf);
if (leaf && mode == XsdConfigMode.IGNORE) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN) {
for (XsdConfigItem child : item.getChildren()) {
appendNodeAsField(classModel, child, fieldCounters, context, allClasses);
}
return;
}
if (shouldInlineSimpleContentNode(item)) {
appendSimpleContentFieldsToParent(classModel, item, fieldCounters, context, allClasses);
return;
}
if (shouldInlineAttributeCarrierWrapperNode(item)) {
appendWrapperChildrenToParent(classModel, item, fieldCounters, context, allClasses);
return;
}
String fieldName = ensureUniqueField(resolveFieldName(item), fieldCounters);
boolean complexChild = !item.isEffectiveLeaf() && !item.isAttribute();
String baseType;
if (complexChild) {
String candidateClassName = sanitizeClassName(toUpperCamel(safe(item.getXmlName())));
String signature = buildClassSignature(item, context.signatureCache, new LinkedHashSet<XsdConfigItem>());
String shapeKey = buildShapeKey(candidateClassName, signature);
String reusedClassName = context.classNameByShapeKey.get(shapeKey);
if (reusedClassName != null) {
baseType = reusedClassName;
} else {
String nestedClassName = ensureUniqueClassName(candidateClassName, context.classCounters);
ClassModel nestedClass = new ClassModel(nestedClassName);
allClasses.add(nestedClass);
context.classNameByShapeKey.put(shapeKey, nestedClassName);
buildClassModel(item, nestedClass, allClasses, context);
baseType = nestedClassName;
}
} else {
baseType = resolveLeafType(item.getJavaType());
}
String fieldType = item.isRepeated() ? "List<" + baseType + ">" : baseType;
classModel.fields.add(new FieldModel(fieldName, fieldType));
}
private boolean shouldInlineSimpleContentNode(XsdConfigItem item) {
return isSimpleContentNode(item) && !item.isRepeated();
}
private boolean isSimpleContentNode(XsdConfigItem node) {
return node != null && !node.isAttribute() && node.hasOnlyAttributeChildren();
}
private void appendSimpleContentFieldsToParent(ClassModel classModel,
XsdConfigItem item,
Map<String, Integer> fieldCounters,
GenerationContext context,
List<ClassModel> allClasses) {
appendSelfValueFieldIfNeeded(item, classModel, fieldCounters);
for (XsdConfigItem child : item.getChildren()) {
appendNodeAsField(classModel, child, fieldCounters, context, allClasses);
}
}
private boolean shouldInlineAttributeCarrierWrapperNode(XsdConfigItem item) {
if (item == null || item.isRepeated() || item.isAttribute() || !item.hasChildren()) {
return false;
}
boolean hasAttributeCarrierChild = false;
for (XsdConfigItem child : item.getChildren()) {
if (!child.isEffectiveLeaf()) {
return false;
}
if (child.hasOnlyAttributeChildren()) {
hasAttributeCarrierChild = true;
}
}
return hasAttributeCarrierChild;
}
private void appendWrapperChildrenToParent(ClassModel classModel,
XsdConfigItem item,
Map<String, Integer> fieldCounters,
GenerationContext context,
List<ClassModel> allClasses) {
for (XsdConfigItem child : item.getChildren()) {
appendNodeAsField(classModel, child, fieldCounters, context, allClasses);
}
}
private String buildClassSignature(XsdConfigItem node,
Map<XsdConfigItem, String> signatureCache,
Set<XsdConfigItem> visiting) {
if (node == null) {
return "";
}
String cached = signatureCache.get(node);
if (cached != null) {
return cached;
}
if (visiting.contains(node)) {
return "RECURSIVE(" + sanitizeClassName(toUpperCamel(safe(node.getXmlName()))) + ")";
}
visiting.add(node);
Map<String, Integer> fieldCounters = new LinkedHashMap<String, Integer>();
List<String> entries = new ArrayList<String>();
appendSelfValueSignatureIfNeeded(node, fieldCounters, entries);
for (XsdConfigItem child : node.getChildren()) {
appendNodeSignatureEntries(child, fieldCounters, entries, signatureCache, visiting);
}
visiting.remove(node);
String signature = String.join("|", entries);
signatureCache.put(node, signature);
return signature;
}
private void appendSelfValueSignatureIfNeeded(XsdConfigItem node,
Map<String, Integer> fieldCounters,
List<String> entries) {
if (!shouldAppendSelfValueField(node)) {
return;
}
String fieldName = ensureUniqueField(resolveFieldName(node), fieldCounters);
String fieldType = resolveLeafType(node.getJavaType());
entries.add(fieldName + ":" + fieldType);
}
private void appendNodeSignatureEntries(XsdConfigItem item,
Map<String, Integer> fieldCounters,
List<String> entries,
Map<XsdConfigItem, String> signatureCache,
Set<XsdConfigItem> visiting) {
boolean leaf = item.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(item, leaf);
if (leaf && mode == XsdConfigMode.IGNORE) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN) {
for (XsdConfigItem child : item.getChildren()) {
appendNodeSignatureEntries(child, fieldCounters, entries, signatureCache, visiting);
}
return;
}
if (shouldInlineSimpleContentNode(item)) {
appendSelfValueSignatureIfNeeded(item, fieldCounters, entries);
for (XsdConfigItem child : item.getChildren()) {
appendNodeSignatureEntries(child, fieldCounters, entries, signatureCache, visiting);
}
return;
}
if (shouldInlineAttributeCarrierWrapperNode(item)) {
for (XsdConfigItem child : item.getChildren()) {
appendNodeSignatureEntries(child, fieldCounters, entries, signatureCache, visiting);
}
return;
}
String fieldName = ensureUniqueField(resolveFieldName(item), fieldCounters);
boolean complexChild = !item.isEffectiveLeaf() && !item.isAttribute();
String baseType;
if (complexChild) {
String childBaseName = sanitizeClassName(toUpperCamel(safe(item.getXmlName())));
String childSignature = buildClassSignature(item, signatureCache, visiting);
baseType = "{" + childBaseName + "#" + childSignature + "}";
} else {
baseType = resolveLeafType(item.getJavaType());
}
String fieldType = item.isRepeated() ? "List<" + baseType + ">" : baseType;
entries.add(fieldName + ":" + fieldType);
}
private String buildShapeKey(String className, String signature) {
return className.toLowerCase(Locale.ROOT) + "|" + signature;
}
private XsdConfigMode effectiveMode(XsdConfigItem item, boolean leaf) {
XsdConfigMode mode = item.getConfigMode();
if (leaf) {
return mode == XsdConfigMode.IGNORE ? XsdConfigMode.IGNORE : XsdConfigMode.KEEP;
}
if (mode == XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES
|| mode == XsdConfigMode.PRESERVE_HIERARCHY
|| mode == XsdConfigMode.FLATTEN
|| mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return mode;
}
if (mode == XsdConfigMode.IGNORE) {
return XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN;
}
return XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
private String renderSource(ClassModel rootModel, String packageName) {
StringBuilder builder = new StringBuilder(2048);
Set<String> imports = new LinkedHashSet<String>();
collectImports(rootModel, imports);
if (packageName != null) {
builder.append("package ").append(packageName).append(";\n\n");
}
for (String anImport : imports) {
builder.append("import ").append(anImport).append(";\n");
}
if (!imports.isEmpty()) {
builder.append("\n");
}
builder.append("public class ").append(rootModel.className).append(" {\n\n");
renderFieldsAndAccessors(builder, rootModel, 1);
builder.append("}\n");
return builder.toString();
}
private void renderFieldsAndAccessors(StringBuilder builder, ClassModel classModel, int indentLevel) {
for (FieldModel field : classModel.fields) {
indent(builder, indentLevel);
builder.append("private ").append(field.type).append(" ").append(field.name).append(";\n");
}
if (!classModel.fields.isEmpty()) {
builder.append("\n");
}
for (FieldModel field : classModel.fields) {
String getterName = "get" + toUpperCamel(field.name);
String setterName = "set" + toUpperCamel(field.name);
indent(builder, indentLevel);
builder.append("public ").append(field.type).append(" ").append(getterName).append("() {\n");
indent(builder, indentLevel + 1);
builder.append("return ").append(field.name).append(";\n");
indent(builder, indentLevel);
builder.append("}\n\n");
indent(builder, indentLevel);
builder.append("public void ").append(setterName).append("(").append(field.type).append(" ").append(field.name).append(") {\n");
indent(builder, indentLevel + 1);
builder.append("this.").append(field.name).append(" = ").append(field.name).append(";\n");
indent(builder, indentLevel);
builder.append("}\n\n");
}
}
private void collectImports(ClassModel classModel, Set<String> imports) {
for (FieldModel field : classModel.fields) {
String type = field.type;
if (type.startsWith("List<")) {
imports.add("java.util.List");
String inner = type.substring("List<".length(), type.length() - 1);
addTypeImport(inner, imports);
} else {
addTypeImport(type, imports);
}
}
}
private void writeSource(File javaFile, String source) throws XomGenerationException {
try (Writer writer = new OutputStreamWriter(new FileOutputStream(javaFile), StandardCharsets.UTF_8)) {
writer.write(source);
writer.flush();
} catch (Exception ex) {
throw new XomGenerationException("XOM-006", "写入Java实体失败: " + ex.getMessage());
}
}
private void addTypeImport(String type, Set<String> imports) {
if (type == null) {
return;
}
if ("BigDecimal".equals(type)) {
imports.add("java.math.BigDecimal");
return;
}
if ("LocalDate".equals(type)) {
imports.add("java.time.LocalDate");
return;
}
if ("LocalDateTime".equals(type)) {
imports.add("java.time.LocalDateTime");
return;
}
if ("LocalTime".equals(type)) {
imports.add("java.time.LocalTime");
return;
}
if (type.contains(".") && !type.startsWith("java.lang.")) {
imports.add(type);
}
}
private String resolveFieldName(XsdConfigItem item) {
String candidate = trimToNull(item.getJavaProperty());
if (candidate == null) {
candidate = toLowerCamel(safe(item.getXmlName()));
}
return sanitizeFieldName(candidate);
}
private String resolveLeafType(String configuredType) {
String type = trimToNull(configuredType);
if (type == null) {
return "String";
}
if ("byte[]".equals(type)) {
return "byte[]";
}
if (type.contains(".")) {
return type;
}
return sanitizeClassName(type);
}
private String ensureUniqueField(String name, Map<String, Integer> counters) {
String key = name.toLowerCase(Locale.ROOT);
Integer counter = counters.get(key);
if (counter == null) {
counters.put(key, 1);
return name;
}
int next = counter + 1;
counters.put(key, next);
return name + next;
}
private String ensureUniqueClassName(String className, Map<String, Integer> counters) {
String key = className.toLowerCase(Locale.ROOT);
Integer counter = counters.get(key);
if (counter == null) {
counters.put(key, 1);
return className;
}
int next = counter + 1;
counters.put(key, next);
return className + next;
}
private String sanitizeClassName(String text) {
String value = trimToNull(text);
if (value == null) {
return "GeneratedEntity";
}
String className = toUpperCamel(value);
if (className.isEmpty()) {
className = "GeneratedEntity";
}
if (!Character.isJavaIdentifierStart(className.charAt(0))) {
className = "C" + className;
}
return stripInvalidIdentifierChars(className);
}
private String sanitizeDeclaredClassName(String text) {
String value = trimToNull(text);
if (value == null) {
return "GeneratedEntity";
}
String className = value
.replaceAll("[^A-Za-z0-9_]+", "_")
.replaceAll("_+", "_")
.replaceAll("^_+|_+$", "");
if (className.isEmpty()) {
return "GeneratedEntity";
}
if (!Character.isJavaIdentifierStart(className.charAt(0))) {
className = "C_" + className;
}
className = stripInvalidIdentifierChars(className);
if (className.isEmpty() || "_".equals(className)) {
return "GeneratedEntity";
}
if (JAVA_KEYWORDS.contains(className.toLowerCase(Locale.ROOT))) {
className = className + "Type";
}
return className;
}
private String sanitizeFieldName(String text) {
String value = trimToNull(text);
if (value == null) {
return "field";
}
String fieldName = toLowerCamel(value);
if (fieldName.isEmpty()) {
fieldName = "field";
}
if (!Character.isJavaIdentifierStart(fieldName.charAt(0))) {
fieldName = "_" + fieldName;
}
fieldName = stripInvalidIdentifierChars(fieldName);
if (JAVA_KEYWORDS.contains(fieldName)) {
fieldName = fieldName + "Field";
}
return fieldName;
}
private String stripInvalidIdentifierChars(String text) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char current = text.charAt(i);
if (i == 0) {
if (Character.isJavaIdentifierStart(current)) {
builder.append(current);
}
continue;
}
if (Character.isJavaIdentifierPart(current)) {
builder.append(current);
}
}
return builder.toString();
}
private void indent(StringBuilder builder, int indentLevel) {
for (int i = 0; i < indentLevel * 4; i++) {
builder.append(' ');
}
}
private String safe(String text) {
return text == null ? "" : text;
}
private String trimToNull(String text) {
if (text == null) {
return null;
}
String trimmed = text.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
private String toUpperCamel(String source) {
if (source == null || source.trim().isEmpty()) {
return "";
}
String cleaned = source.replaceAll("[^A-Za-z0-9]+", " ").trim();
if (cleaned.isEmpty()) {
return "";
}
String[] parts = cleaned.split("\\s+");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (part.isEmpty()) {
continue;
}
if (part.length() == 1) {
builder.append(part.toUpperCase(Locale.ROOT));
} else {
builder.append(part.substring(0, 1).toUpperCase(Locale.ROOT));
builder.append(part.substring(1));
}
}
return builder.toString();
}
private String toLowerCamel(String source) {
String upper = toUpperCamel(source);
if (upper.isEmpty()) {
return "";
}
if (isAllUpperCaseLetters(upper)) {
return upper.toLowerCase(Locale.ROOT);
}
if (upper.length() == 1) {
return upper.toLowerCase(Locale.ROOT);
}
return upper.substring(0, 1).toLowerCase(Locale.ROOT) + upper.substring(1);
}
private boolean isAllUpperCaseLetters(String text) {
boolean hasLetter = false;
for (int i = 0; i < text.length(); i++) {
char current = text.charAt(i);
if (!Character.isLetter(current)) {
continue;
}
hasLetter = true;
if (!Character.isUpperCase(current)) {
return false;
}
}
return hasLetter;
}
private static class JavaTypeDescriptor {
private final String packageName;
private final String className;
private JavaTypeDescriptor(String packageName, String className) {
this.packageName = packageName;
this.className = className;
}
}
private static class ClassModel {
private final String className;
private final List<FieldModel> fields = new ArrayList<FieldModel>();
private ClassModel(String className) {
this.className = className;
}
}
private static class GenerationContext {
private final Map<String, Integer> classCounters = new LinkedHashMap<String, Integer>();
private final Map<String, String> classNameByShapeKey = new LinkedHashMap<String, String>();
private final Map<XsdConfigItem, String> signatureCache = new IdentityHashMap<XsdConfigItem, String>();
}
private static class FieldModel {
private final String name;
private final String type;
private FieldModel(String name, String type) {
this.name = name;
this.type = type;
}
}
private static final Set<String> JAVA_KEYWORDS = new LinkedHashSet<String>(Arrays.asList(
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class",
"const", "continue", "default", "do", "double", "else", "enum", "extends", "final",
"finally", "float", "for", "goto", "if", "implements", "import", "instanceof",
"int", "interface", "long", "native", "new", "package", "private", "protected",
"public", "return", "short", "static", "strictfp", "super", "switch", "synchronized",
"this", "throw", "throws", "transient", "try", "void", "volatile", "while"
));
}

View File

@@ -0,0 +1,168 @@
package com.dmiki.metaplugin.xom;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import java.util.ArrayList;
import java.io.File;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
public class XomConfigValidator {
public void validate(XomGenerateOptions options, XsdConfigItem root) throws XomGenerationException {
if (root == null) {
throw new XomGenerationException("XOM-003", "未解析到XSD节点结构");
}
if (options == null) {
throw new XomGenerationException("XOM-002", "生成参数为空");
}
String xsdPath = trimToNull(options.getXsdPath());
if (xsdPath == null) {
throw new XomGenerationException("XOM-001", "未提供XSD路径");
}
File xsdFile = new File(xsdPath);
if (!xsdFile.isFile()) {
throw new XomGenerationException("XOM-001", "XSD文件不存在或不可读: " + xsdPath);
}
String schemaId = trimToNull(options.getSchemaId());
if (schemaId == null) {
throw new XomGenerationException("XOM-002", "schemaId不能为空");
}
if (options.getMaxDepth() < 1) {
throw new XomGenerationException("XOM-005", "maxDepth必须大于0");
}
if (options.isStrictMode() && trimToNull(options.getResultMapType()) == null) {
throw new XomGenerationException("XOM-004", "strictMode=true 时 resultMap.type 不能为空");
}
if (!hasRenderableNode(root)) {
throw new XomGenerationException("XOM-003", "所有节点都被忽略无法生成result映射");
}
validateTree(root, options, 1);
}
private boolean hasRenderableNode(XsdConfigItem item) {
if (item == null) {
return false;
}
boolean leaf = item.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(item, leaf);
if (leaf) {
return mode != XsdConfigMode.IGNORE;
}
if (mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return false;
}
if (mode == XsdConfigMode.FLATTEN) {
for (XsdConfigItem child : item.getChildren()) {
if (hasRenderableNode(child)) {
return true;
}
}
return false;
}
return true;
}
private void validateTree(XsdConfigItem item, XomGenerateOptions options, int depth) throws XomGenerationException {
if (depth > options.getMaxDepth()) {
throw new XomGenerationException("XOM-005", "节点层级超出最大深度: " + item.getXmlName());
}
boolean leaf = item.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(item, leaf);
if (leaf && mode == XsdConfigMode.IGNORE) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return;
}
boolean renderCurrentNode = leaf
|| mode == XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES
|| mode == XsdConfigMode.PRESERVE_HIERARCHY;
if (options.isFailOnUnknownType() && renderCurrentNode) {
String javaType = trimToNull(item.getJavaType());
if (javaType == null) {
throw new XomGenerationException("XOM-004", "节点Java类型缺失: " + item.getXmlName());
}
}
if (leaf) {
return;
}
if (options.isStrictMode()) {
Set<String> childLabels = new LinkedHashSet<String>();
List<XsdConfigItem> directOutputChildren = collectDirectOutputChildren(item, options, depth);
for (XsdConfigItem child : directOutputChildren) {
String childName = trimToNull(child.getXmlName());
if (childName != null && !childLabels.add(childName)) {
throw new XomGenerationException("XOM-003",
"同级节点存在重复label: " + childName + " (父节点: " + item.getXmlName() + ")");
}
}
}
for (XsdConfigItem child : item.getChildren()) {
validateTree(child, options, depth + 1);
}
}
private List<XsdConfigItem> collectDirectOutputChildren(XsdConfigItem parent, XomGenerateOptions options, int depth) {
List<XsdConfigItem> output = new ArrayList<XsdConfigItem>();
for (XsdConfigItem child : parent.getChildren()) {
boolean leaf = child.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(child, leaf);
if ((leaf && mode == XsdConfigMode.IGNORE)
|| (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN)) {
continue;
}
if (options.isFlattenChoice() && child.isChoiceContainer() && !child.isEffectiveLeaf() && depth < options.getMaxDepth()) {
output.addAll(collectDirectOutputChildren(child, options, depth + 1));
continue;
}
if (!leaf && mode == XsdConfigMode.FLATTEN) {
output.addAll(collectDirectOutputChildren(child, options, depth + 1));
continue;
}
output.add(child);
}
return output;
}
private XsdConfigMode effectiveMode(XsdConfigItem item, boolean leaf) {
XsdConfigMode mode = item.getConfigMode();
if (leaf) {
return mode == XsdConfigMode.IGNORE ? XsdConfigMode.IGNORE : XsdConfigMode.KEEP;
}
if (mode == XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES
|| mode == XsdConfigMode.PRESERVE_HIERARCHY
|| mode == XsdConfigMode.FLATTEN
|| mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return mode;
}
if (mode == XsdConfigMode.IGNORE) {
return XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN;
}
return XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
private String trimToNull(String text) {
if (text == null) {
return null;
}
String trimmed = text.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
}

View File

@@ -0,0 +1,141 @@
package com.dmiki.metaplugin.xom;
public class XomGenerateOptions {
private final String projectBasePath;
private final String messageName;
private final String schemaId;
private final String resultMapType;
private final String xsdPath;
private final String outputPath;
private final boolean strictMode;
private final boolean prettyPrint;
private final boolean overwrite;
private final boolean failOnUnknownType;
private final boolean includeAnnotations;
private final boolean flattenChoice;
private final int maxDepth;
private final String customAttribute;
private final String preProcessor;
public XomGenerateOptions(String projectBasePath,
String messageName,
String schemaId,
String resultMapType,
String xsdPath,
String outputPath,
boolean strictMode,
boolean prettyPrint,
boolean overwrite,
boolean failOnUnknownType,
boolean includeAnnotations,
boolean flattenChoice,
int maxDepth) {
this(projectBasePath,
messageName,
schemaId,
resultMapType,
xsdPath,
outputPath,
strictMode,
prettyPrint,
overwrite,
failOnUnknownType,
includeAnnotations,
flattenChoice,
maxDepth,
null,
null);
}
public XomGenerateOptions(String projectBasePath,
String messageName,
String schemaId,
String resultMapType,
String xsdPath,
String outputPath,
boolean strictMode,
boolean prettyPrint,
boolean overwrite,
boolean failOnUnknownType,
boolean includeAnnotations,
boolean flattenChoice,
int maxDepth,
String customAttribute,
String preProcessor) {
this.projectBasePath = projectBasePath;
this.messageName = messageName;
this.schemaId = schemaId;
this.resultMapType = resultMapType;
this.xsdPath = xsdPath;
this.outputPath = outputPath;
this.strictMode = strictMode;
this.prettyPrint = prettyPrint;
this.overwrite = overwrite;
this.failOnUnknownType = failOnUnknownType;
this.includeAnnotations = includeAnnotations;
this.flattenChoice = flattenChoice;
this.maxDepth = maxDepth;
this.customAttribute = customAttribute;
this.preProcessor = preProcessor;
}
public String getProjectBasePath() {
return projectBasePath;
}
public String getMessageName() {
return messageName;
}
public String getSchemaId() {
return schemaId;
}
public String getResultMapType() {
return resultMapType;
}
public String getXsdPath() {
return xsdPath;
}
public String getOutputPath() {
return outputPath;
}
public boolean isStrictMode() {
return strictMode;
}
public boolean isPrettyPrint() {
return prettyPrint;
}
public boolean isOverwrite() {
return overwrite;
}
public boolean isFailOnUnknownType() {
return failOnUnknownType;
}
public boolean isIncludeAnnotations() {
return includeAnnotations;
}
public boolean isFlattenChoice() {
return flattenChoice;
}
public int getMaxDepth() {
return maxDepth;
}
public String getCustomAttribute() {
return customAttribute;
}
public String getPreProcessor() {
return preProcessor;
}
}

View File

@@ -0,0 +1,14 @@
package com.dmiki.metaplugin.xom;
public final class XomGenerationDefaults {
public static final boolean STRICT_MODE = true;
public static final boolean PRETTY_PRINT = true;
public static final boolean OVERWRITE = true;
public static final boolean FAIL_ON_UNKNOWN_TYPE = false;
public static final boolean INCLUDE_ANNOTATIONS = false;
public static final boolean FLATTEN_CHOICE = false;
public static final int MAX_DEPTH = 64;
private XomGenerationDefaults() {
}
}

View File

@@ -0,0 +1,14 @@
package com.dmiki.metaplugin.xom;
public class XomGenerationException extends Exception {
private final String code;
public XomGenerationException(String code, String message) {
super("[" + code + "] " + message);
this.code = code;
}
public String getCode() {
return code;
}
}

View File

@@ -0,0 +1,21 @@
package com.dmiki.metaplugin.xom;
import java.io.File;
public class XomGenerationResult {
private final File mappingXmlFile;
private final File javaEntityFile;
public XomGenerationResult(File mappingXmlFile, File javaEntityFile) {
this.mappingXmlFile = mappingXmlFile;
this.javaEntityFile = javaEntityFile;
}
public File getMappingXmlFile() {
return mappingXmlFile;
}
public File getJavaEntityFile() {
return javaEntityFile;
}
}

View File

@@ -0,0 +1,159 @@
package com.dmiki.metaplugin.xom;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class XomGenerationService {
private final XomConfigValidator validator = new XomConfigValidator();
private final XomXmlRenderer renderer = new XomXmlRenderer();
private final JavaEntityGenerator javaEntityGenerator = new JavaEntityGenerator();
public String preview(XsdConfigItem root, XomGenerateOptions options) throws XomGenerationException {
validator.validate(options, root);
return renderer.render(root, options);
}
public XomGenerationResult generate(XsdConfigItem root, XomGenerateOptions options) throws XomGenerationException {
validator.validate(options, root);
String outputPath = resolveOutputPath(options);
File outputFile = new File(outputPath);
if (outputFile.exists() && !options.isOverwrite()) {
throw new XomGenerationException("XOM-006", "输出文件已存在且overwrite=false: " + outputPath);
}
File parent = outputFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new XomGenerationException("XOM-006", "无法创建输出目录: " + parent.getAbsolutePath());
}
String xml = renderer.render(root, options);
try (Writer writer = new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8)) {
writer.write(xml);
writer.flush();
} catch (Exception ex) {
throw new XomGenerationException("XOM-006", "写入输出文件失败: " + ex.getMessage());
}
File javaEntityFile = shouldGenerateJavaEntity(options)
? javaEntityGenerator.generate(root, options, resolveJavaOutputDirectory(outputFile))
: null;
return new XomGenerationResult(outputFile, javaEntityFile);
}
private File resolveJavaOutputDirectory(File mappingXmlFile) {
File mappingDirectory = mappingXmlFile == null ? null : mappingXmlFile.getParentFile();
if (mappingDirectory == null) {
return new File("java");
}
return new File(mappingDirectory, "java");
}
private boolean shouldGenerateJavaEntity(XomGenerateOptions options) {
String resultMapType = trimToNull(options.getResultMapType());
if (resultMapType == null) {
return false;
}
int index = resultMapType.lastIndexOf('.');
return index > 0;
}
private String resolveOutputPath(XomGenerateOptions options) throws XomGenerationException {
String outputPath = trimToNull(options.getOutputPath());
if (outputPath != null) {
File outputFile = resolveOutputFile(outputPath, options);
if (outputFile.isDirectory()) {
return new File(outputFile, safeName(options.getSchemaId()) + ".xml").getAbsolutePath();
}
return outputFile.getAbsolutePath();
}
File resourcesDir = resolveResourcesDirectory(options);
File targetDirectory = resolveNextMsgMapperDirectory(resourcesDir);
String schemaId = trimToNull(options.getSchemaId());
if (schemaId == null) {
schemaId = nameFromXsd(options);
}
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");
}
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++;
}
}
private String nameFromXsd(XomGenerateOptions options) {
String xsdPath = trimToNull(options.getXsdPath());
if (xsdPath == null) {
return "mapping";
}
File xsdFile = new File(xsdPath);
String fileName = xsdFile.getName();
int index = fileName.lastIndexOf('.');
if (index > 0) {
fileName = fileName.substring(0, index);
}
return fileName;
}
private String safeName(String text) {
String fallback = trimToNull(text);
if (fallback == null) {
return "result";
}
return fallback.replaceAll("[^A-Za-z0-9_.-]+", "_");
}
private String trimToNull(String text) {
if (text == null) {
return null;
}
String trimmed = text.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
}

View File

@@ -0,0 +1,271 @@
package com.dmiki.metaplugin.xom;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
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 {
public String render(XsdConfigItem root, XomGenerateOptions options) {
boolean prettyPrint = options.isPrettyPrint();
String newline = prettyPrint ? "\n" : "";
String renderedXsdPath = resolveRenderedXsdPath(options);
StringBuilder builder = new StringBuilder(1024);
builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>").append(newline);
appendMessageOpen(builder, options, prettyPrint, 0, newline);
appendSchema(builder, options, renderedXsdPath, prettyPrint, 1, newline);
appendResultMapOpen(builder, options, prettyPrint, 1, newline);
List<XsdConfigItem> topResults = root == null
? new ArrayList<XsdConfigItem>()
: collectIncludedChildren(root, options, 1);
for (XsdConfigItem child : topResults) {
appendResult(builder, child, options, prettyPrint, 2, 2, newline, null);
}
appendResultMapClose(builder, prettyPrint, 1, newline);
appendMessageClose(builder, prettyPrint, 0, newline);
if (prettyPrint && builder.length() > 0 && builder.charAt(builder.length() - 1) != '\n') {
builder.append('\n');
}
return builder.toString();
}
private void appendMessageOpen(StringBuilder builder,
XomGenerateOptions options,
boolean prettyPrint,
int indentLevel,
String newline) {
appendIndent(builder, prettyPrint, indentLevel);
builder.append("<message>").append(newline);
}
private void appendMessageClose(StringBuilder builder,
boolean prettyPrint,
int indentLevel,
String newline) {
appendIndent(builder, prettyPrint, indentLevel);
builder.append("</message>").append(newline);
}
private void appendSchema(StringBuilder builder,
XomGenerateOptions options,
String renderedXsdPath,
boolean prettyPrint,
int indentLevel,
String newline) {
appendIndent(builder, prettyPrint, indentLevel);
builder.append("<schema");
appendXmlAttr(builder, "id", safe(options.getSchemaId()));
appendXmlAttr(builder, "xsdPath", safe(renderedXsdPath));
builder.append("/>").append(newline);
}
private void appendResultMapOpen(StringBuilder builder,
XomGenerateOptions options,
boolean prettyPrint,
int indentLevel,
String newline) {
appendIndent(builder, prettyPrint, indentLevel);
builder.append("<resultMap");
appendXmlAttr(builder, "id", safe(options.getSchemaId()));
String type = trimToNull(options.getResultMapType());
appendXmlAttr(builder, "type", type == null ? "java.lang.Object" : type);
builder.append(">").append(newline);
}
private void appendResultMapClose(StringBuilder builder,
boolean prettyPrint,
int indentLevel,
String newline) {
appendIndent(builder, prettyPrint, indentLevel);
builder.append("</resultMap>").append(newline);
}
private void appendResult(StringBuilder builder,
XsdConfigItem item,
XomGenerateOptions options,
boolean prettyPrint,
int indentLevel,
int depth,
String newline,
String labelOverride) {
if (item == null) {
return;
}
if (depth > options.getMaxDepth()) {
return;
}
boolean leaf = item.isEffectiveLeaf();
boolean attributeCarrierNode = !item.isAttribute() && item.hasOnlyAttributeChildren();
XsdConfigMode mode = effectiveMode(item, leaf);
if (leaf && mode == XsdConfigMode.IGNORE) {
return;
}
if (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return;
}
List<XsdConfigItem> children = (leaf && !attributeCarrierNode)
? new ArrayList<XsdConfigItem>()
: collectIncludedChildren(item, options, depth);
if (!leaf && mode == XsdConfigMode.FLATTEN) {
String parentLabel = safe(item.getXmlName());
for (XsdConfigItem child : children) {
String childLabel = safe(child.getXmlName());
String flattenedLabel = parentLabel + ":" + childLabel;
appendResult(builder, child, options, prettyPrint, indentLevel, depth + 1, newline, flattenedLabel);
}
return;
}
boolean includeProperty = leaf || mode == XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES;
appendIndent(builder, prettyPrint, indentLevel);
builder.append("<result");
if (includeProperty) {
String javaProperty = trimToNull(item.getJavaProperty());
if (javaProperty != null) {
appendXmlAttr(builder, "property", javaProperty);
}
}
String renderedLabel = trimToNull(labelOverride);
if (renderedLabel == null) {
renderedLabel = safe(item.getXmlName());
}
String labelKey = item.isAttribute() ? "lable-attribute" : "lable";
appendXmlAttr(builder, labelKey, renderedLabel);
if (item.isCustomAttributeEnabled()) {
appendXmlAttr(builder, "attribute",
safe(resolveCellValue(item.getCustomAttributeValue(), options.getCustomAttribute())));
}
if (item.isPreProcessorEnabled()) {
appendXmlAttr(builder, "pre-processor",
safe(resolveCellValue(item.getPreProcessorValue(), options.getPreProcessor())));
}
if (children.isEmpty()) {
builder.append("/>").append(newline);
return;
}
builder.append(">").append(newline);
for (XsdConfigItem child : children) {
appendResult(builder, child, options, prettyPrint, indentLevel + 1, depth + 1, newline, null);
}
appendIndent(builder, prettyPrint, indentLevel);
builder.append("</result>").append(newline);
}
private List<XsdConfigItem> collectIncludedChildren(XsdConfigItem parent, XomGenerateOptions options, int depth) {
List<XsdConfigItem> included = new ArrayList<XsdConfigItem>();
for (XsdConfigItem child : parent.getChildren()) {
boolean leaf = child.isEffectiveLeaf();
XsdConfigMode mode = effectiveMode(child, leaf);
if ((leaf && mode == XsdConfigMode.IGNORE)
|| (!leaf && mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN)) {
continue;
}
if (options.isFlattenChoice() && child.isChoiceContainer() && !child.isEffectiveLeaf() && depth < options.getMaxDepth()) {
included.addAll(collectIncludedChildren(child, options, depth + 1));
continue;
}
included.add(child);
}
return included;
}
private XsdConfigMode effectiveMode(XsdConfigItem item, boolean leaf) {
XsdConfigMode mode = item.getConfigMode();
if (leaf) {
return mode == XsdConfigMode.IGNORE ? XsdConfigMode.IGNORE : XsdConfigMode.KEEP;
}
if (mode == XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES
|| mode == XsdConfigMode.PRESERVE_HIERARCHY
|| mode == XsdConfigMode.FLATTEN
|| mode == XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN) {
return mode;
}
// Backward compatibility: historical non-leaf mode "IGNORE" means drop this subtree.
if (mode == XsdConfigMode.IGNORE) {
return XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN;
}
return XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
private void appendXmlAttr(StringBuilder builder, String key, String value) {
builder.append(" ").append(key).append("=\"").append(escapeXml(value)).append("\"");
}
private void appendIndent(StringBuilder builder, boolean prettyPrint, int indentLevel) {
if (!prettyPrint) {
return;
}
for (int i = 0; i < indentLevel * 4; i++) {
builder.append(' ');
}
}
private String safe(String text) {
return text == null ? "" : text;
}
private String trimToNull(String text) {
if (text == null) {
return null;
}
String trimmed = text.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
private String resolveCellValue(String rowValue, String globalValue) {
String cell = trimToNull(rowValue);
if (cell != null) {
return cell;
}
return trimToNull(globalValue);
}
private String escapeXml(String value) {
if (value == null) {
return "";
}
return value
.replace("&", "&amp;")
.replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;");
}
private String resolveRenderedXsdPath(XomGenerateOptions options) {
String rawXsdPath = trimToNull(options.getXsdPath());
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;
}
}
}

View File

@@ -0,0 +1,485 @@
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.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
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 {
private final XsdSchemaParser parser = new XsdSchemaParser();
@Override
public XomReverseDisplayResult loadForDisplay(File mappingXmlFile, String projectBasePath) throws Exception {
MappingDocument mappingDocument = parseMappingDocument(mappingXmlFile);
File xsdFile = resolveXsdFile(mappingDocument.getXsdPath(), mappingXmlFile, projectBasePath);
XsdConfigItem root = parser.parse(xsdFile);
prepareForReplay(root);
applyReplay(root, mappingDocument.getResults());
return new XomReverseDisplayResult(
root,
xsdFile,
mappingDocument.getSchemaId(),
mappingDocument.getResultMapType(),
inferTopInput(mappingDocument.getCustomAttributeValues()),
inferTopInput(mappingDocument.getPreProcessorValues())
);
}
private MappingDocument parseMappingDocument(File mappingXmlFile) throws Exception {
if (mappingXmlFile == null || !mappingXmlFile.isFile()) {
throw new IllegalArgumentException("XOM文件不存在: " + mappingXmlFile);
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
Document document = factory.newDocumentBuilder().parse(mappingXmlFile);
Element root = document.getDocumentElement();
Element schema = findFirstDirectChildByLocalName(root, "schema");
Element resultMap = findFirstDirectChildByLocalName(root, "resultMap");
if (resultMap == null) {
throw new IllegalArgumentException("XOM文件缺少resultMap节点");
}
String schemaId = trimToNull(attribute(schema, "id"));
if (schemaId == null) {
schemaId = trimToNull(resultMap.getAttribute("id"));
}
String xsdPath = trimToNull(attribute(schema, "xsdPath"));
String resultMapType = trimToNull(resultMap.getAttribute("type"));
Set<String> customAttributeValues = new LinkedHashSet<String>();
Set<String> preProcessorValues = new LinkedHashSet<String>();
List<ResultNode> results = parseResultChildren(resultMap, customAttributeValues, preProcessorValues);
return new MappingDocument(schemaId, resultMapType, xsdPath, results, customAttributeValues, preProcessorValues);
}
private void prepareForReplay(XsdConfigItem item) {
if (item == null) {
return;
}
item.setConfigMode(item.isEffectiveLeaf() ? XsdConfigMode.IGNORE : XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN);
item.setCustomAttributeEnabled(false);
item.setCustomAttributeValue(null);
item.setPreProcessorEnabled(false);
item.setPreProcessorValue(null);
for (XsdConfigItem child : item.getChildren()) {
prepareForReplay(child);
}
}
private void applyReplay(XsdConfigItem root, List<ResultNode> topResults) {
if (root == null) {
return;
}
List<ResultNode> pool = new ArrayList<ResultNode>(topResults);
ResultNode direct = removeFirstMatch(pool, root);
if (direct != null) {
applyDirectConfig(root, direct);
applyChildrenReplay(root, new ArrayList<ResultNode>(direct.getChildren()));
return;
}
if (root.isEffectiveLeaf()) {
root.setConfigMode(XsdConfigMode.IGNORE);
return;
}
if (hasAnySubtreeMatch(root, pool)) {
root.setConfigMode(XsdConfigMode.FLATTEN);
applyChildrenReplay(root, pool);
return;
}
root.setConfigMode(XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN);
}
private void applyChildrenReplay(XsdConfigItem parent, List<ResultNode> pool) {
for (XsdConfigItem child : parent.getChildren()) {
ResultNode direct = removeFirstMatch(pool, child);
if (direct != null) {
applyDirectConfig(child, direct);
applyChildrenReplay(child, new ArrayList<ResultNode>(direct.getChildren()));
continue;
}
if (child.isEffectiveLeaf()) {
child.setConfigMode(XsdConfigMode.IGNORE);
continue;
}
if (hasAnySubtreeMatch(child, pool)) {
child.setConfigMode(XsdConfigMode.FLATTEN);
applyChildrenReplay(child, pool);
} else {
child.setConfigMode(XsdConfigMode.FLATTEN_AND_IGNORE_CHILDREN);
}
}
}
private boolean hasAnySubtreeMatch(XsdConfigItem node, List<ResultNode> pool) {
for (XsdConfigItem child : node.getChildren()) {
if (hasDirectMatch(child, pool)) {
return true;
}
if (!child.isEffectiveLeaf() && hasAnySubtreeMatch(child, pool)) {
return true;
}
}
return false;
}
private boolean hasDirectMatch(XsdConfigItem item, List<ResultNode> pool) {
for (ResultNode node : pool) {
if (matches(item, node)) {
return true;
}
}
return false;
}
private ResultNode removeFirstMatch(List<ResultNode> pool, XsdConfigItem item) {
for (int i = 0; i < pool.size(); i++) {
ResultNode node = pool.get(i);
if (matches(item, node)) {
pool.remove(i);
return node;
}
}
return null;
}
private boolean matches(XsdConfigItem item, ResultNode node) {
if (item == null || node == null) {
return false;
}
String expected = trimToNull(item.getXmlName());
if (expected == null) {
return false;
}
String actual = item.isAttribute()
? firstNotNull(node.getLabelAttribute(), node.getLabel())
: firstNotNull(node.getLabel(), node.getLabelAttribute());
if (actual == null) {
return false;
}
return expected.equals(actual) || expected.equalsIgnoreCase(actual);
}
private void applyDirectConfig(XsdConfigItem item, ResultNode node) {
String property = trimToNull(node.getProperty());
item.setJavaProperty(property);
String customAttribute = trimToNull(node.getCustomAttribute());
item.setCustomAttributeEnabled(customAttribute != null);
item.setCustomAttributeValue(customAttribute);
String preProcessor = trimToNull(node.getPreProcessor());
item.setPreProcessorEnabled(preProcessor != null);
item.setPreProcessorValue(preProcessor);
if (item.isEffectiveLeaf()) {
item.setConfigMode(XsdConfigMode.KEEP);
} else if (property != null) {
item.setConfigMode(XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES);
} else {
item.setConfigMode(XsdConfigMode.PRESERVE_HIERARCHY);
}
}
private File resolveXsdFile(String xsdPath, File mappingXmlFile, String projectBasePath) {
if (xsdPath == null) {
throw new IllegalArgumentException("XOM文件缺少schema.xsdPath配置");
}
List<File> candidates = new ArrayList<File>();
File declared = new File(xsdPath);
if (declared.isAbsolute()) {
candidates.add(declared);
}
File mappingParent = mappingXmlFile == null ? null : mappingXmlFile.getParentFile();
if (mappingParent != null) {
candidates.add(new File(mappingParent, xsdPath));
}
File resourcesRoot = 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));
}
Set<String> dedup = new LinkedHashSet<String>();
for (File candidate : candidates) {
if (candidate == null) {
continue;
}
File normalized = candidate.getAbsoluteFile();
String key = normalized.getAbsolutePath();
if (!dedup.add(key)) {
continue;
}
if (normalized.isFile()) {
return normalized;
}
}
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) {
List<ResultNode> nodes = new ArrayList<ResultNode>();
if (parent == null) {
return nodes;
}
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
if (!"result".equals(localNameOf(child))) {
continue;
}
ResultNode resultNode = parseResultNode(child, customAttributeValues, preProcessorValues);
nodes.add(resultNode);
}
return nodes;
}
private ResultNode parseResultNode(Element element,
Set<String> customAttributeValues,
Set<String> preProcessorValues) {
String label = trimToNull(element.getAttribute("label"));
if (label == null) {
label = trimToNull(element.getAttribute("lable"));
}
String labelAttribute = trimToNull(element.getAttribute("label-attribute"));
if (labelAttribute == null) {
labelAttribute = trimToNull(element.getAttribute("lable-attribute"));
}
String property = trimToNull(element.getAttribute("property"));
String customAttribute = trimToNull(element.getAttribute("attribute"));
String preProcessor = trimToNull(element.getAttribute("pre-processor"));
if (customAttribute != null) {
customAttributeValues.add(customAttribute);
}
if (preProcessor != null) {
preProcessorValues.add(preProcessor);
}
List<ResultNode> children = parseResultChildren(element, customAttributeValues, preProcessorValues);
return new ResultNode(label, labelAttribute, property, customAttribute, preProcessor, children);
}
private Element findFirstDirectChildByLocalName(Element parent, String localName) {
if (parent == null || localName == null) {
return null;
}
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element element = (Element) node;
if (localName.equals(localNameOf(element))) {
return element;
}
}
return null;
}
private String localNameOf(Node node) {
if (node == null) {
return "";
}
String localName = node.getLocalName();
if (localName != null) {
return localName;
}
String nodeName = node.getNodeName();
if (nodeName == null) {
return "";
}
int index = nodeName.indexOf(':');
return index >= 0 ? nodeName.substring(index + 1) : nodeName;
}
private String attribute(Element element, String name) {
if (element == null || name == null) {
return null;
}
return element.getAttribute(name);
}
private String inferTopInput(Set<String> values) {
if (values == null || values.isEmpty()) {
return null;
}
if (values.size() == 1) {
return values.iterator().next();
}
return null;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
private String firstNotNull(String first, String second) {
String firstValue = trimToNull(first);
if (firstValue != null) {
return firstValue;
}
return trimToNull(second);
}
private static final class MappingDocument {
private final String schemaId;
private final String resultMapType;
private final String xsdPath;
private final List<ResultNode> results;
private final Set<String> customAttributeValues;
private final Set<String> preProcessorValues;
private MappingDocument(String schemaId,
String resultMapType,
String xsdPath,
List<ResultNode> results,
Set<String> customAttributeValues,
Set<String> preProcessorValues) {
this.schemaId = schemaId;
this.resultMapType = resultMapType;
this.xsdPath = xsdPath;
this.results = results;
this.customAttributeValues = customAttributeValues;
this.preProcessorValues = preProcessorValues;
}
private String getSchemaId() {
return schemaId;
}
private String getResultMapType() {
return resultMapType;
}
private String getXsdPath() {
return xsdPath;
}
private List<ResultNode> getResults() {
return results;
}
private Set<String> getCustomAttributeValues() {
return customAttributeValues;
}
private Set<String> getPreProcessorValues() {
return preProcessorValues;
}
}
private static final class ResultNode {
private final String label;
private final String labelAttribute;
private final String property;
private final String customAttribute;
private final String preProcessor;
private final List<ResultNode> children;
private ResultNode(String label,
String labelAttribute,
String property,
String customAttribute,
String preProcessor,
List<ResultNode> children) {
this.label = label;
this.labelAttribute = labelAttribute;
this.property = property;
this.customAttribute = customAttribute;
this.preProcessor = preProcessor;
this.children = children;
}
private String getLabel() {
return label;
}
private String getLabelAttribute() {
return labelAttribute;
}
private String getProperty() {
return property;
}
private String getCustomAttribute() {
return customAttribute;
}
private String getPreProcessor() {
return preProcessor;
}
private List<ResultNode> getChildren() {
return children;
}
}
}

View File

@@ -0,0 +1,52 @@
package com.dmiki.metaplugin.xom.reverse;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import java.io.File;
public class XomReverseDisplayResult {
private final XsdConfigItem root;
private final File xsdFile;
private final String schemaId;
private final String resultMapType;
private final String customAttribute;
private final String preProcessor;
public XomReverseDisplayResult(XsdConfigItem root,
File xsdFile,
String schemaId,
String resultMapType,
String customAttribute,
String preProcessor) {
this.root = root;
this.xsdFile = xsdFile;
this.schemaId = schemaId;
this.resultMapType = resultMapType;
this.customAttribute = customAttribute;
this.preProcessor = preProcessor;
}
public XsdConfigItem getRoot() {
return root;
}
public File getXsdFile() {
return xsdFile;
}
public String getSchemaId() {
return schemaId;
}
public String getResultMapType() {
return resultMapType;
}
public String getCustomAttribute() {
return customAttribute;
}
public String getPreProcessor() {
return preProcessor;
}
}

View File

@@ -0,0 +1,8 @@
package com.dmiki.metaplugin.xom.reverse;
import java.io.File;
public interface XomReverseDisplayService {
XomReverseDisplayResult loadForDisplay(File mappingXmlFile, String projectBasePath) throws Exception;
}

View File

@@ -0,0 +1,149 @@
package com.dmiki.metaplugin.xsd.model;
import java.util.ArrayList;
import java.util.List;
public class XsdConfigItem {
private final String xmlName;
private final boolean required;
private final boolean repeated;
private final boolean attribute;
private final List<XsdConfigItem> children = new ArrayList<XsdConfigItem>();
private XsdConfigMode configMode = XsdConfigMode.KEEP;
private String javaProperty;
private String javaType;
private boolean choiceContainer;
private boolean customAttributeEnabled;
private boolean preProcessorEnabled;
private String customAttributeValue;
private String preProcessorValue;
public XsdConfigItem(String xmlName, boolean required, boolean repeated, boolean attribute) {
this.xmlName = xmlName;
this.required = required;
this.repeated = repeated;
this.attribute = attribute;
}
public String getXmlName() {
return xmlName;
}
public boolean isRequired() {
return required;
}
public boolean isRepeated() {
return repeated;
}
public boolean isAttribute() {
return attribute;
}
public List<XsdConfigItem> getChildren() {
return children;
}
public XsdConfigMode getConfigMode() {
return configMode;
}
public void setConfigMode(XsdConfigMode configMode) {
this.configMode = configMode;
}
public String getJavaProperty() {
return javaProperty;
}
public void setJavaProperty(String javaProperty) {
this.javaProperty = javaProperty;
}
public String getJavaType() {
return javaType;
}
public void setJavaType(String javaType) {
this.javaType = javaType;
}
public boolean isChoiceContainer() {
return choiceContainer;
}
public void setChoiceContainer(boolean choiceContainer) {
this.choiceContainer = choiceContainer;
}
public boolean isCustomAttributeEnabled() {
return customAttributeEnabled;
}
public void setCustomAttributeEnabled(boolean customAttributeEnabled) {
this.customAttributeEnabled = customAttributeEnabled;
}
public String getCustomAttributeValue() {
return customAttributeValue;
}
public void setCustomAttributeValue(String customAttributeValue) {
this.customAttributeValue = customAttributeValue;
}
public boolean isPreProcessorEnabled() {
return preProcessorEnabled;
}
public void setPreProcessorEnabled(boolean preProcessorEnabled) {
this.preProcessorEnabled = preProcessorEnabled;
}
public String getPreProcessorValue() {
return preProcessorValue;
}
public void setPreProcessorValue(String preProcessorValue) {
this.preProcessorValue = preProcessorValue;
}
public boolean hasChildren() {
return !children.isEmpty();
}
public boolean hasNonAttributeChildren() {
for (XsdConfigItem child : children) {
if (!child.isAttribute()) {
return true;
}
}
return false;
}
public boolean hasOnlyAttributeChildren() {
return hasChildren() && !hasNonAttributeChildren();
}
public boolean isEffectiveLeaf() {
return !hasNonAttributeChildren();
}
public String getDisplayName() {
StringBuilder builder = new StringBuilder();
if (required) {
builder.append("* ");
}
if (attribute) {
builder.append("@");
}
builder.append(xmlName);
return builder.toString();
}
@Override
public String toString() {
return getDisplayName();
}
}

View File

@@ -0,0 +1,90 @@
package com.dmiki.metaplugin.xsd.model;
public enum XsdConfigMode {
PRESERVE_HIERARCHY_AND_ATTRIBUTES("保留层级保留属性", false, true),
PRESERVE_HIERARCHY("保留层级不保留属性", false, true),
FLATTEN("不保留层级", false, true),
FLATTEN_AND_IGNORE_CHILDREN("不保留层级且不保留子项", false, true),
KEEP("保留", true, false),
IGNORE("不保留", true, false);
private final String label;
private final boolean leafAvailable;
private final boolean nonLeafAvailable;
XsdConfigMode(String label, boolean leafAvailable, boolean nonLeafAvailable) {
this.label = label;
this.leafAvailable = leafAvailable;
this.nonLeafAvailable = nonLeafAvailable;
}
public String getLabel() {
return label;
}
public boolean isLeafAvailable() {
return leafAvailable;
}
public boolean isNonLeafAvailable() {
return nonLeafAvailable;
}
public static XsdConfigMode fromLabel(String label) {
if (label == null) {
return KEEP;
}
String normalized = label.trim();
for (XsdConfigMode mode : values()) {
if (mode.label.equals(normalized)) {
return mode;
}
}
// Backward compatibility for historical label.
if ("忽略".equals(normalized)) {
return IGNORE;
}
return KEEP;
}
public static String[] labels() {
XsdConfigMode[] values = values();
String[] labels = new String[values.length];
for (int i = 0; i < values.length; i++) {
labels[i] = values[i].label;
}
return labels;
}
public static String[] nonLeafLabels() {
return new String[]{
PRESERVE_HIERARCHY_AND_ATTRIBUTES.getLabel(),
PRESERVE_HIERARCHY.getLabel(),
FLATTEN.getLabel(),
FLATTEN_AND_IGNORE_CHILDREN.getLabel()
};
}
public static String[] leafLabels() {
return new String[]{
KEEP.getLabel(),
IGNORE.getLabel()
};
}
public static XsdConfigMode normalizeForNode(boolean leaf, XsdConfigMode mode) {
if (mode == null) {
return leaf ? KEEP : PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
if (leaf) {
return mode.isLeafAvailable() ? mode : KEEP;
}
if (mode == IGNORE) {
return FLATTEN_AND_IGNORE_CHILDREN;
}
if (mode == KEEP) {
return PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
return mode.isNonLeafAvailable() ? mode : PRESERVE_HIERARCHY_AND_ATTRIBUTES;
}
}

View File

@@ -0,0 +1,628 @@
package com.dmiki.metaplugin.xsd.parser;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class XsdSchemaParser {
private static final String DOCUMENT_ELEMENT_NAME = "Document";
private static final String UNBOUNDED = "unbounded";
public XsdConfigItem parse(File xsdFile) throws Exception {
if (xsdFile == null || !xsdFile.isFile()) {
throw new IllegalArgumentException("XSD文件不存在: " + xsdFile);
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
Document document = factory.newDocumentBuilder().parse(xsdFile);
Element schema = document.getDocumentElement();
SchemaIndex index = buildIndex(schema);
Element rootElement = resolveRootElement(index);
XsdConfigItem root = buildNodeFromElement(rootElement, index, new LinkedHashSet<String>());
applyDefaultConfigMode(root);
normalizeCcyAttributeProperties(root);
return root;
}
private SchemaIndex buildIndex(Element schema) {
SchemaIndex index = new SchemaIndex();
NodeList children = schema.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
String localName = localNameOf(child);
String name = child.getAttribute("name");
if (name == null || name.trim().isEmpty()) {
continue;
}
if ("element".equals(localName)) {
index.globalElements.put(name, child);
} else if ("complexType".equals(localName)) {
index.complexTypes.put(name, child);
} else if ("simpleType".equals(localName)) {
index.simpleTypes.put(name, child);
}
}
return index;
}
private Element resolveRootElement(SchemaIndex index) {
Element root = index.globalElements.get(DOCUMENT_ELEMENT_NAME);
if (root != null) {
return root;
}
if (index.globalElements.isEmpty()) {
throw new IllegalStateException("未找到全局xs:element定义无法构建树结构");
}
return index.globalElements.values().iterator().next();
}
private XsdConfigItem buildNodeFromElement(Element element, SchemaIndex index, LinkedHashSet<String> typeStack) {
String name = element.getAttribute("name");
if (name == null || name.isEmpty()) {
name = "AnonymousElement";
}
int minOccurs = parseIntOrDefault(element.getAttribute("minOccurs"), 1);
String maxOccursRaw = element.getAttribute("maxOccurs");
boolean repeated = !maxOccursRaw.isEmpty() && (!"1".equals(maxOccursRaw));
if (UNBOUNDED.equalsIgnoreCase(maxOccursRaw)) {
repeated = true;
}
XsdConfigItem item = new XsdConfigItem(name, minOccurs > 0, repeated, false);
item.setJavaProperty(toLowerCamel(name));
String typeName = normalizeTypeName(element.getAttribute("type"));
Element inlineComplexType = firstDirectChildByLocalName(element, "complexType");
Element inlineSimpleType = firstDirectChildByLocalName(element, "simpleType");
if (inlineComplexType != null) {
item.setJavaType(toUpperCamel(name));
populateFromComplexType(item, inlineComplexType, index, typeStack);
} else if (typeName != null && index.complexTypes.containsKey(typeName)) {
item.setJavaType(cleanComplexJavaType(typeName, name));
expandComplexTypeByName(item, typeName, index, typeStack);
} else if (inlineSimpleType != null) {
item.setJavaType(resolveSimpleTypeJavaType(inlineSimpleType, index));
} else if (typeName != null) {
item.setJavaType(resolveTypeByName(typeName, index, name));
} else {
item.setJavaType("String");
}
if (item.isEffectiveLeaf() && item.getConfigMode() == null) {
item.setConfigMode(XsdConfigMode.KEEP);
}
return item;
}
private void expandComplexTypeByName(XsdConfigItem item,
String typeName,
SchemaIndex index,
LinkedHashSet<String> typeStack) {
if (typeStack.contains(typeName)) {
return;
}
Element complexType = index.complexTypes.get(typeName);
if (complexType == null) {
return;
}
typeStack.add(typeName);
try {
populateFromComplexType(item, complexType, index, typeStack);
} finally {
typeStack.remove(typeName);
}
}
private void populateFromComplexType(XsdConfigItem parent,
Element complexType,
SchemaIndex index,
LinkedHashSet<String> typeStack) {
NodeList children = complexType.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
String localName = localNameOf(child);
if ("sequence".equals(localName) || "choice".equals(localName) || "all".equals(localName)) {
if ("choice".equals(localName)) {
parent.setChoiceContainer(true);
}
collectParticleChildren(parent, child, index, typeStack, localName);
} else if ("complexContent".equals(localName)) {
collectFromComplexContent(parent, child, index, typeStack);
} else if ("simpleContent".equals(localName)) {
collectFromSimpleContent(parent, child, index, typeStack);
} else if ("attribute".equals(localName)) {
parent.getChildren().add(buildNodeFromAttribute(child, index));
} else if ("element".equals(localName)) {
parent.getChildren().add(buildNodeFromElement(child, index, typeStack));
}
}
}
private void collectFromSimpleContent(XsdConfigItem parent,
Element simpleContent,
SchemaIndex index,
LinkedHashSet<String> typeStack) {
NodeList children = simpleContent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element element = (Element) node;
String localName = localNameOf(element);
if (!"extension".equals(localName) && !"restriction".equals(localName)) {
continue;
}
String baseType = normalizeTypeName(element.getAttribute("base"));
if (baseType != null && index.complexTypes.containsKey(baseType)) {
expandComplexTypeByName(parent, baseType, index, typeStack);
}
if (baseType != null && (isBuiltInXsdType(baseType) || index.simpleTypes.containsKey(baseType))) {
parent.setJavaType(resolveTypeByName(baseType, index, parent.getXmlName()));
}
NodeList extensionChildren = element.getChildNodes();
for (int j = 0; j < extensionChildren.getLength(); j++) {
Node extensionNode = extensionChildren.item(j);
if (!(extensionNode instanceof Element)) {
continue;
}
Element extensionElement = (Element) extensionNode;
String extensionLocal = localNameOf(extensionElement);
if ("attribute".equals(extensionLocal)) {
parent.getChildren().add(buildNodeFromAttribute(extensionElement, index));
}
}
}
}
private void collectFromComplexContent(XsdConfigItem parent,
Element complexContent,
SchemaIndex index,
LinkedHashSet<String> typeStack) {
NodeList children = complexContent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element element = (Element) node;
String localName = localNameOf(element);
if (!"extension".equals(localName) && !"restriction".equals(localName)) {
continue;
}
String baseType = normalizeTypeName(element.getAttribute("base"));
if (baseType != null && index.complexTypes.containsKey(baseType)) {
expandComplexTypeByName(parent, baseType, index, typeStack);
}
NodeList extensionChildren = element.getChildNodes();
for (int j = 0; j < extensionChildren.getLength(); j++) {
Node extensionNode = extensionChildren.item(j);
if (!(extensionNode instanceof Element)) {
continue;
}
Element extensionElement = (Element) extensionNode;
String extensionLocal = localNameOf(extensionElement);
if ("sequence".equals(extensionLocal) || "choice".equals(extensionLocal) || "all".equals(extensionLocal)) {
if ("choice".equals(extensionLocal)) {
parent.setChoiceContainer(true);
}
collectParticleChildren(parent, extensionElement, index, typeStack, extensionLocal);
} else if ("attribute".equals(extensionLocal)) {
parent.getChildren().add(buildNodeFromAttribute(extensionElement, index));
} else if ("element".equals(extensionLocal)) {
parent.getChildren().add(buildNodeFromElement(extensionElement, index, typeStack));
}
}
}
}
private void collectParticleChildren(XsdConfigItem parent,
Element particle,
SchemaIndex index,
LinkedHashSet<String> typeStack,
String particleType) {
NodeList children = particle.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
String localName = localNameOf(child);
if ("element".equals(localName)) {
XsdConfigItem item = buildNodeFromElement(child, index, typeStack);
if ("choice".equals(particleType) && !item.isEffectiveLeaf()) {
item.setChoiceContainer(true);
}
parent.getChildren().add(item);
} else if ("choice".equals(localName) || "sequence".equals(localName) || "all".equals(localName)) {
if ("choice".equals(localName)) {
parent.setChoiceContainer(true);
}
collectParticleChildren(parent, child, index, typeStack, localName);
} else if ("attribute".equals(localName)) {
parent.getChildren().add(buildNodeFromAttribute(child, index));
}
}
}
private XsdConfigItem buildNodeFromAttribute(Element attribute, SchemaIndex index) {
String name = attribute.getAttribute("name");
if (name == null || name.isEmpty()) {
name = "AnonymousAttribute";
}
boolean required = "required".equalsIgnoreCase(attribute.getAttribute("use"));
XsdConfigItem item = new XsdConfigItem(name, required, false, true);
item.setJavaProperty(toLowerCamel(name));
String typeName = normalizeTypeName(attribute.getAttribute("type"));
if (typeName == null) {
item.setJavaType("String");
} else {
item.setJavaType(resolveTypeByName(typeName, index, name));
}
item.setConfigMode(XsdConfigMode.KEEP);
return item;
}
private String resolveTypeByName(String typeName, SchemaIndex index, String fallbackName) {
if (isBuiltInXsdType(typeName)) {
return mapXsdBuiltInType(typeName);
}
Element simpleType = index.simpleTypes.get(typeName);
if (simpleType != null) {
return resolveSimpleTypeJavaType(simpleType, index);
}
if (index.complexTypes.containsKey(typeName)) {
return cleanComplexJavaType(typeName, fallbackName);
}
return toUpperCamel(typeName);
}
private String resolveSimpleTypeJavaType(Element simpleType, SchemaIndex index) {
Element restriction = firstDirectChildByLocalName(simpleType, "restriction");
if (restriction != null) {
String base = normalizeTypeName(restriction.getAttribute("base"));
if (base != null) {
return resolveTypeByName(base, index, simpleType.getAttribute("name"));
}
}
Element union = firstDirectChildByLocalName(simpleType, "union");
if (union != null) {
return "String";
}
return "String";
}
private boolean isBuiltInXsdType(String typeName) {
if (typeName == null) {
return false;
}
String lower = typeName.toLowerCase(Locale.ROOT);
return lower.startsWith("xs:") || lower.startsWith("xsd:")
|| "string".equals(lower)
|| "int".equals(lower)
|| "integer".equals(lower)
|| "long".equals(lower)
|| "short".equals(lower)
|| "byte".equals(lower)
|| "decimal".equals(lower)
|| "double".equals(lower)
|| "float".equals(lower)
|| "boolean".equals(lower)
|| "date".equals(lower)
|| "datetime".equals(lower)
|| "time".equals(lower)
|| "base64binary".equals(lower);
}
private String mapXsdBuiltInType(String rawTypeName) {
String typeName = normalizeTypeName(rawTypeName);
if (typeName == null) {
return "String";
}
String lower = typeName.toLowerCase(Locale.ROOT);
if ("string".equals(lower) || "token".equals(lower) || "normalizedstring".equals(lower) || "anyuri".equals(lower)) {
return "String";
}
if ("boolean".equals(lower)) {
return "Boolean";
}
if ("byte".equals(lower)) {
return "Byte";
}
if ("short".equals(lower)) {
return "Short";
}
if ("int".equals(lower) || "integer".equals(lower) || "nonnegativeinteger".equals(lower) || "positiveinteger".equals(lower)) {
return "Integer";
}
if ("long".equals(lower)) {
return "Long";
}
if ("float".equals(lower)) {
return "Float";
}
if ("double".equals(lower)) {
return "Double";
}
if ("decimal".equals(lower)) {
return "BigDecimal";
}
if ("date".equals(lower)) {
return "LocalDate";
}
if ("datetime".equals(lower)) {
return "LocalDateTime";
}
if ("time".equals(lower)) {
return "LocalTime";
}
if ("base64binary".equals(lower)) {
return "byte[]";
}
return "String";
}
private String cleanComplexJavaType(String rawTypeName, String fallback) {
String normalized = normalizeTypeName(rawTypeName);
if (normalized == null || normalized.isEmpty()) {
return toUpperCamel(fallback);
}
int suffixIndex = normalized.indexOf("__");
if (suffixIndex > 0) {
normalized = normalized.substring(0, suffixIndex);
}
return toUpperCamel(normalized);
}
private String normalizeTypeName(String typeName) {
if (typeName == null || typeName.trim().isEmpty()) {
return null;
}
int idx = typeName.indexOf(':');
if (idx >= 0 && idx < typeName.length() - 1) {
return typeName.substring(idx + 1);
}
return typeName;
}
private int parseIntOrDefault(String text, int defaultValue) {
if (text == null || text.trim().isEmpty()) {
return defaultValue;
}
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException ex) {
return defaultValue;
}
}
private Element firstDirectChildByLocalName(Element parent, String localName) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element element = (Element) node;
if (localName.equals(localNameOf(element))) {
return element;
}
}
return null;
}
private String localNameOf(Node node) {
String localName = node.getLocalName();
if (localName != null) {
return localName;
}
String nodeName = node.getNodeName();
if (nodeName == null) {
return "";
}
int idx = nodeName.indexOf(':');
return idx >= 0 ? nodeName.substring(idx + 1) : nodeName;
}
private void applyDefaultConfigMode(XsdConfigItem root) {
if (root == null) {
return;
}
if (!root.isEffectiveLeaf()) {
root.setConfigMode(XsdConfigMode.PRESERVE_HIERARCHY_AND_ATTRIBUTES);
for (XsdConfigItem child : root.getChildren()) {
applyDefaultConfigMode(child);
}
} else {
root.setConfigMode(XsdConfigMode.KEEP);
}
}
private void normalizeCcyAttributeProperties(XsdConfigItem root) {
if (root == null) {
return;
}
normalizeCcyAttributePropertiesByEntityScope(root);
}
private void normalizeCcyAttributePropertiesByEntityScope(XsdConfigItem entityNode) {
List<XsdConfigItem> ccyAttributes = new ArrayList<XsdConfigItem>();
collectCcyAttributesInCurrentEntity(entityNode, ccyAttributes);
if (ccyAttributes.size() == 1) {
ccyAttributes.get(0).setJavaProperty("ccy");
} else if (ccyAttributes.size() > 1) {
for (int i = 0; i < ccyAttributes.size(); i++) {
ccyAttributes.get(i).setJavaProperty("ccy" + (i + 1));
}
}
for (XsdConfigItem child : entityNode.getChildren()) {
if (isNestedEntityNode(child)) {
normalizeCcyAttributePropertiesByEntityScope(child);
}
}
}
private void collectCcyAttributesInCurrentEntity(XsdConfigItem entityNode, List<XsdConfigItem> output) {
if (entityNode == null) {
return;
}
for (XsdConfigItem child : entityNode.getChildren()) {
collectCcyFromInlinedNode(child, output);
}
}
private void collectCcyFromInlinedNode(XsdConfigItem node, List<XsdConfigItem> output) {
if (node == null) {
return;
}
if (node.isAttribute()) {
if ("ccy".equalsIgnoreCase(node.getXmlName())) {
output.add(node);
}
return;
}
if (shouldInlineSimpleContentNode(node) || shouldInlineAttributeCarrierWrapperNode(node)) {
for (XsdConfigItem child : node.getChildren()) {
collectCcyFromInlinedNode(child, output);
}
}
}
private boolean isNestedEntityNode(XsdConfigItem node) {
if (node == null || node.isAttribute() || !node.hasChildren()) {
return false;
}
return !shouldInlineSimpleContentNode(node) && !shouldInlineAttributeCarrierWrapperNode(node);
}
private boolean shouldInlineSimpleContentNode(XsdConfigItem node) {
return isSimpleContentNode(node) && !node.isRepeated();
}
private boolean isSimpleContentNode(XsdConfigItem node) {
return node != null && !node.isAttribute() && node.hasOnlyAttributeChildren();
}
private boolean shouldInlineAttributeCarrierWrapperNode(XsdConfigItem node) {
if (node == null || node.isRepeated() || node.isAttribute() || !node.hasChildren()) {
return false;
}
boolean hasAttributeCarrierChild = false;
for (XsdConfigItem child : node.getChildren()) {
if (!child.isEffectiveLeaf()) {
return false;
}
if (child.hasOnlyAttributeChildren()) {
hasAttributeCarrierChild = true;
}
}
return hasAttributeCarrierChild;
}
private String toLowerCamel(String source) {
String upperCamel = toUpperCamel(source);
if (upperCamel.isEmpty()) {
return upperCamel;
}
if (isAllUpperCaseLetters(upperCamel)) {
return upperCamel.toLowerCase(Locale.ROOT);
}
if (upperCamel.length() == 1) {
return upperCamel.toLowerCase(Locale.ROOT);
}
return upperCamel.substring(0, 1).toLowerCase(Locale.ROOT) + upperCamel.substring(1);
}
private boolean isAllUpperCaseLetters(String text) {
boolean hasLetter = false;
for (int i = 0; i < text.length(); i++) {
char current = text.charAt(i);
if (!Character.isLetter(current)) {
continue;
}
hasLetter = true;
if (!Character.isUpperCase(current)) {
return false;
}
}
return hasLetter;
}
private String toUpperCamel(String source) {
if (source == null || source.trim().isEmpty()) {
return "";
}
String cleaned = source.replaceAll("[^A-Za-z0-9]+", " ").trim();
if (cleaned.isEmpty()) {
return source;
}
String[] parts = cleaned.split("\\s+");
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (part.isEmpty()) {
continue;
}
if (part.length() == 1) {
builder.append(part.toUpperCase(Locale.ROOT));
} else {
builder.append(part.substring(0, 1).toUpperCase(Locale.ROOT));
builder.append(part.substring(1));
}
}
return builder.toString();
}
private static class SchemaIndex {
private final Map<String, Element> globalElements = new LinkedHashMap<String, Element>();
private final Map<String, Element> complexTypes = new LinkedHashMap<String, Element>();
private final Map<String, Element> simpleTypes = new LinkedHashMap<String, Element>();
}
public static String[] javaTypeOptions() {
return new String[]{
"String",
"Integer",
"Long",
"BigDecimal",
"Boolean",
"LocalDate",
"LocalDateTime",
"LocalTime",
"Double",
"Float",
"byte[]"
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,215 @@
package com.dmiki.metaplugin.xsd.ui;
import com.dmiki.metaplugin.xsd.model.XsdConfigItem;
import com.dmiki.metaplugin.xsd.model.XsdConfigMode;
import com.intellij.ui.treeStructure.treetable.ListTreeTableModelOnColumns;
import com.intellij.ui.treeStructure.treetable.TreeTableModel;
import com.intellij.util.ui.ColumnInfo;
import javax.swing.tree.DefaultMutableTreeNode;
import java.util.List;
public class XsdTreeTableModel extends ListTreeTableModelOnColumns {
private static final ColumnInfo[] COLUMNS = new ColumnInfo[]{
new ColumnInfo<DefaultMutableTreeNode, String>("XML节点") {
@Override
public String valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
return item == null ? "" : item.getDisplayName();
}
@Override
public Class<?> getColumnClass() {
return TreeTableModel.class;
}
},
new ColumnInfo<DefaultMutableTreeNode, String>("配置策略") {
@Override
public String valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
return isConfigVisible(node) && item != null ? item.getConfigMode().getLabel() : "";
}
@Override
public boolean isCellEditable(DefaultMutableTreeNode node) {
return isConfigVisible(node);
}
@Override
public void setValue(DefaultMutableTreeNode node, String value) {
XsdConfigItem item = itemOf(node);
if (isConfigVisible(node) && item != null && value != null) {
XsdConfigMode selected = XsdConfigMode.fromLabel(value);
item.setConfigMode(XsdConfigMode.normalizeForNode(item.isEffectiveLeaf(), selected));
}
}
},
new ColumnInfo<DefaultMutableTreeNode, String>("JAVA属性名") {
@Override
public String valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
return isConfigVisible(node) && item != null ? item.getJavaProperty() : "";
}
@Override
public boolean isCellEditable(DefaultMutableTreeNode node) {
return isConfigVisible(node);
}
@Override
public void setValue(DefaultMutableTreeNode node, String value) {
XsdConfigItem item = itemOf(node);
if (isConfigVisible(node) && item != null) {
item.setJavaProperty(value == null ? "" : value.trim());
}
}
},
new ColumnInfo<DefaultMutableTreeNode, String>("JAVA类型") {
@Override
public String valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
return isConfigVisible(node) && item != null ? item.getJavaType() : "";
}
@Override
public boolean isCellEditable(DefaultMutableTreeNode node) {
return isConfigVisible(node);
}
@Override
public void setValue(DefaultMutableTreeNode node, String value) {
XsdConfigItem item = itemOf(node);
if (isConfigVisible(node) && item != null) {
item.setJavaType(value == null ? "" : value.trim());
}
}
},
new ColumnInfo<DefaultMutableTreeNode, ToggleInputValue>("自定义属性") {
@Override
public ToggleInputValue valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
if (!isConfigVisible(node) || item == null) {
return ToggleInputValue.EMPTY;
}
return new ToggleInputValue(item.isCustomAttributeEnabled(), item.getCustomAttributeValue());
}
@Override
public boolean isCellEditable(DefaultMutableTreeNode node) {
return isConfigVisible(node);
}
@Override
public void setValue(DefaultMutableTreeNode node, ToggleInputValue value) {
XsdConfigItem item = itemOf(node);
if (isConfigVisible(node) && item != null) {
item.setCustomAttributeEnabled(value != null && value.isEnabled());
item.setCustomAttributeValue(value == null ? null : value.getText());
}
}
@Override
public Class<?> getColumnClass() {
return ToggleInputValue.class;
}
},
new ColumnInfo<DefaultMutableTreeNode, ToggleInputValue>("预处理属性") {
@Override
public ToggleInputValue valueOf(DefaultMutableTreeNode node) {
XsdConfigItem item = itemOf(node);
if (!isConfigVisible(node) || item == null) {
return ToggleInputValue.EMPTY;
}
return new ToggleInputValue(item.isPreProcessorEnabled(), item.getPreProcessorValue());
}
@Override
public boolean isCellEditable(DefaultMutableTreeNode node) {
return isConfigVisible(node);
}
@Override
public void setValue(DefaultMutableTreeNode node, ToggleInputValue value) {
XsdConfigItem item = itemOf(node);
if (isConfigVisible(node) && item != null) {
item.setPreProcessorEnabled(value != null && value.isEnabled());
item.setPreProcessorValue(value == null ? null : value.getText());
}
}
@Override
public Class<?> getColumnClass() {
return ToggleInputValue.class;
}
}
};
public XsdTreeTableModel(DefaultMutableTreeNode root) {
super(root, COLUMNS);
}
public static DefaultMutableTreeNode toSwingTree(XsdConfigItem root) {
DefaultMutableTreeNode swingRoot = new DefaultMutableTreeNode(root);
appendChildren(swingRoot, root.getChildren());
return swingRoot;
}
private static void appendChildren(DefaultMutableTreeNode parent, List<XsdConfigItem> children) {
XsdConfigItem parentItem = itemOf(parent);
boolean inlineAttributeChildren = shouldInlineAttributeChildren(parentItem);
for (XsdConfigItem child : children) {
if (inlineAttributeChildren && child.isAttribute()) {
continue;
}
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(child);
parent.add(childNode);
appendChildren(childNode, child.getChildren());
}
}
private static boolean shouldInlineAttributeChildren(XsdConfigItem item) {
if (item == null || !item.hasChildren()) {
return false;
}
return item.hasOnlyAttributeChildren();
}
private static XsdConfigItem itemOf(DefaultMutableTreeNode node) {
if (node == null) {
return null;
}
Object userObject = node.getUserObject();
if (userObject instanceof XsdConfigItem) {
return (XsdConfigItem) userObject;
}
return null;
}
private static boolean isConfigVisible(DefaultMutableTreeNode node) {
return itemOf(node) != null && !isRootNode(node);
}
private static boolean isRootNode(DefaultMutableTreeNode node) {
return node != null && node.getParent() == null;
}
public static final class ToggleInputValue {
public static final ToggleInputValue EMPTY = new ToggleInputValue(false, null);
private final boolean enabled;
private final String text;
public ToggleInputValue(boolean enabled, String text) {
this.enabled = enabled;
this.text = text;
}
public boolean isEnabled() {
return enabled;
}
public String getText() {
return text;
}
}
}