Files
meta-plugin/src/main/java/com/dmiki/metaplugin/xom/JavaEntityGenerator.java

649 lines
25 KiB
Java
Raw Normal View History

2026-03-27 10:02:59 +08:00
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"
));
}