!105 开源之夏-根据索引结构生成领域模型

Merge pull request !105 from hwy0907/master
This commit is contained in:
elasticsearch 2024-09-10 13:52:53 +00:00 committed by Gitee
commit 24420a35bf
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
9 changed files with 446 additions and 2 deletions

View File

@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dromara.easyes.starter.config.EsAutoConfiguration,\
org.dromara.easyes.starter.factory.IndexStrategyFactory,\
org.dromara.easyes.starter.service.impl.AutoProcessIndexSmoothlyServiceImpl,\
org.dromara.easyes.starter.service.impl.AutoProcessIndexNotSmoothlyServiceImpl
org.dromara.easyes.starter.service.impl.AutoProcessIndexNotSmoothlyServiceImpl,\
org.dromara.easyes.core.toolkit.Generator

View File

@ -1,4 +1,5 @@
org.dromara.easyes.starter.config.EsAutoConfiguration
org.dromara.easyes.starter.factory.IndexStrategyFactory
org.dromara.easyes.starter.service.impl.AutoProcessIndexSmoothlyServiceImpl
org.dromara.easyes.starter.service.impl.AutoProcessIndexNotSmoothlyServiceImpl
org.dromara.easyes.starter.service.impl.AutoProcessIndexNotSmoothlyServiceImpl
org.dromara.easyes.core.toolkit.Generator

View File

@ -0,0 +1,29 @@
package org.dromara.easyes.core.config;
import lombok.Data;
/**
* generator config 代码生成器配置项
*
* @author hwy
**/
@Data
public class GeneratorConfig {
/**
* indexName 需要生成的索引名称
*/
private String indexName;
/**
* dest package path 生成模型的目标包路径
*/
private String destPackage;
/**
* enable underline to camel case true by default, 是否开启下划线转驼峰 默认开启
*/
private boolean enableUnderlineToCamelCase = true;
/**
* enable lombok true by default, 是否开启lombok 默认开启
*/
private boolean enableLombok = true;
}

View File

@ -0,0 +1,179 @@
package org.dromara.easyes.core.toolkit;
import org.dromara.easyes.common.utils.LogUtils;
import org.dromara.easyes.common.utils.StringUtils;
import org.dromara.easyes.core.config.GeneratorConfig;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import static org.dromara.easyes.common.constants.BaseEsConstants.*;
/**
* entity代码生成器
*
* @author hwy
**/
public class EntityGenerator {
private static final String UNDERLINE = "_";
private static final String USER_DIR = "user.dir";
private static final String SRC = "src";
private static final String MAIN = "main";
private static final String JAVA = "java";
private static final String PACKAGE = "package %s;\n\n";
private static final char PACKAGE_SEPARATOR = '.';
private static final String LOMBOK_IMPORT = "import lombok.Data;\n";
private static final String EE_IMPORT = "import org.dromara.easyes.annotation.*;\n\n";
private static final Pattern ILLEGAL_FIELD_NAME_PATTERN = Pattern.compile("^[^a-zA-Z_$]|[\\W&&[^_]]");
private static final String LOMBOK_TEMPLATE = "public class %s {\n" +
" // Fields\n" +
"%s" +
"}\n";
private static final String TEMPLATE = "public class %s {\n" +
" // Fields\n" +
"%s" +
"\n" +
" // Getters and Setters\n" +
"%s" +
"\n" +
"%s" +
"\n" +
"}\n";
private static final String CLASS_ANNOTATION = "@IndexName(\"%s\")\n" + "@Settings(shardsNum = %d, replicasNum = %d)\n";
private static final String LOMBOK_ANNOTATION = "@Data\n";
private static final String FIELD_TEMPLATE = " private %s %s;\n";
private static final String GETTER_TEMPLATE = " public %s get%s() {\n" +
" return this.%s;\n" +
" }\n";
private static final String SETTER_TEMPLATE = " public void set%s(%s %s) {\n" +
" this.%s = %s;\n" +
" }\n";
private static final String JAVA_SUFFIX = ".java";
/**
* entity 生成器
*
* @param config 配置
* @param fields 字段及类型映射
* @param className 生成的类名
* @param shardsNum 分片
* @param replicasNum 副本
* @throws IOException 异常
*/
public static void generateEntity(GeneratorConfig config, Map<String, String> fields, String className, Integer shardsNum, Integer replicasNum) throws IOException {
// build path info
className = config.isEnableUnderlineToCamelCase() ? capitalize(StringUtils.underlineToCamel(className))
: capitalize(className);
String indexName = config.isEnableUnderlineToCamelCase() ? capitalize(StringUtils.underlineToCamel(config.getIndexName()))
: capitalize(config.getIndexName());
// main class
boolean mainClass = Objects.equals(className, indexName);
String wholePath = System.getProperty(USER_DIR) + File.separator + SRC + File.separator + MAIN + File.separator + JAVA + File.separator + packageToPath(config.getDestPackage());
Path outputPath = Paths.get(wholePath, className + JAVA_SUFFIX);
if (!Files.exists(outputPath.getParent())) {
Files.createDirectories(outputPath.getParent());
}
StringBuilder fieldBuilder = new StringBuilder();
StringBuilder getterBuilder = new StringBuilder();
StringBuilder setterBuilder = new StringBuilder();
// build field info
for (Map.Entry<String, String> entry : fields.entrySet()) {
String fieldName = entry.getKey();
if (config.isEnableUnderlineToCamelCase()) {
fieldName = StringUtils.underlineToCamel(fieldName);
}
// escape illegal sign
fieldName = sanitizeFieldName(fieldName);
String fieldType = entry.getValue();
fieldBuilder.append(String.format(FIELD_TEMPLATE, fieldType, fieldName));
if (!config.isEnableLombok()) {
getterBuilder.append(String.format(GETTER_TEMPLATE, fieldType, capitalize(fieldName), fieldName));
setterBuilder.append(String.format(SETTER_TEMPLATE, capitalize(fieldName), fieldType, fieldName, fieldName, fieldName));
}
}
// build content
String content;
if (config.isEnableLombok()) {
content = String.format(PACKAGE, config.getDestPackage()) + LOMBOK_IMPORT + EE_IMPORT + addIndexAnnotation(indexName, shardsNum, replicasNum, mainClass) +
LOMBOK_ANNOTATION + String.format(LOMBOK_TEMPLATE, className, fieldBuilder);
} else {
content = String.format(PACKAGE, config.getDestPackage()) + EE_IMPORT + addIndexAnnotation(indexName, shardsNum, replicasNum, mainClass) +
String.format(TEMPLATE, className, fieldBuilder, getterBuilder, setterBuilder);
}
// write class to file
try (FileWriter writer = new FileWriter(outputPath.toFile())) {
writer.write(content);
LogUtils.info("Generated entity class: " + outputPath);
}
}
/**
* 将Java包名转换为对应的文件系统路径
*
* @param packageName Java包的全名 "com.example.project.module"
* @return 对应于文件系统的路径使用正确的路径分隔符
*/
private static String packageToPath(String packageName) {
// 将包名中的点.替换为当前系统的路径分隔符
return packageName.replace(PACKAGE_SEPARATOR, File.separatorChar);
}
/**
* 首字母大写
*
* @param str 原字符串
* @return 首字母大写后的字符串
*/
private static String capitalize(String str) {
return Character.toUpperCase(str.charAt(ZERO)) + str.substring(ONE);
}
/**
* 移除字符串中的非法字符使其适合作为Java类的字段名
*
* @param fieldName 原始字段名字符串
* @return 符合Java命名规范的字段名
*/
private static String sanitizeFieldName(String fieldName) {
// 使用正则表达式替换非法字符为空字符同时确保字段名不以数字开头
String sanitized = ILLEGAL_FIELD_NAME_PATTERN.matcher(fieldName).replaceAll(EMPTY_STR);
if (Character.isDigit(sanitized.charAt(ZERO))) {
// 如果第一个字符是数字前缀加上下划线
sanitized = UNDERLINE + sanitized;
}
return sanitized;
}
private static String addIndexAnnotation(String indexName, Integer shardsNum, Integer replicasNum, boolean mainClass) {
if (mainClass) {
return String.format(CLASS_ANNOTATION, indexName, shardsNum, replicasNum);
} else {
return EMPTY_STR;
}
}
}

View File

@ -0,0 +1,132 @@
package org.dromara.easyes.core.toolkit;
import lombok.SneakyThrows;
import org.dromara.easyes.common.utils.CollectionUtils;
import org.dromara.easyes.common.utils.StringUtils;
import org.dromara.easyes.core.biz.EsIndexInfo;
import org.dromara.easyes.core.config.GeneratorConfig;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import static org.dromara.easyes.common.constants.BaseEsConstants.ZERO;
/**
* generator 代码生成器
*
* @author hwy
**/
@Component
public class Generator {
private final static String PROPERTIES = "properties";
private final static String TYPE = "type";
private final static String NESTED = "nested";
private final static String NESTED_SUFFIX1 = "s";
private final static String NESTED_SUFFIX2 = "List";
private final static Set<String> SKIP_TYPE = new HashSet<>(Arrays.asList("join"));
@Autowired
private RestHighLevelClient client;
public Boolean generate(GeneratorConfig config) {
// generateEntityClass
generateEntity(config);
// TODO generate others in future
return Boolean.TRUE;
}
/**
* generate model entity 生成实体类
*
* @param config 配置
*/
@SneakyThrows
private void generateEntity(GeneratorConfig config) {
// get index info
EsIndexInfo esIndexInfo = IndexUtils.getIndexInfo(client, config.getIndexName());
Map<String, Object> mapping = esIndexInfo.getMapping();
if (CollectionUtils.isEmpty(mapping)) {
return;
}
// parse fields info
LinkedHashMap<String, LinkedHashMap<String, Object>> properties = (LinkedHashMap<String, LinkedHashMap<String, Object>>) mapping.get(PROPERTIES);
// execute generate
executeGenerate(properties, config, esIndexInfo, config.getIndexName());
}
@SneakyThrows
private void executeGenerate(LinkedHashMap<String, LinkedHashMap<String, Object>> properties, GeneratorConfig config, EsIndexInfo esIndexInfo, String className) {
Map<String, String> modelMap = new HashMap<>(properties.size());
properties.forEach((k, v) -> Optional.ofNullable(v.get(TYPE)).ifPresent(esType -> {
if (SKIP_TYPE.contains(esType.toString())) {
return;
}
if (NESTED.equals(esType.toString())) {
// nested recursion
executeGenerate((LinkedHashMap<String, LinkedHashMap<String, Object>>) v.get(PROPERTIES), config, esIndexInfo, parseClassName(k));
}
String javaType = getJavaType(esType.toString(), k);
modelMap.put(k, javaType);
}));
// do generate entity
EntityGenerator.generateEntity(config, modelMap, className, esIndexInfo.getShardsNum(), esIndexInfo.getReplicasNum());
}
private static String parseClassName(String origin) {
if (StringUtils.isEmpty(origin)) {
return origin;
}
if (origin.endsWith(NESTED_SUFFIX1)) {
return FieldUtils.firstToUpperCase(origin.substring(ZERO, origin.lastIndexOf(NESTED_SUFFIX1)));
} else if (origin.endsWith(NESTED_SUFFIX2)) {
return FieldUtils.firstToUpperCase(origin.substring(ZERO, origin.lastIndexOf(NESTED_SUFFIX2)));
}
return origin;
}
private static String getJavaType(String esType, String key) {
if (StringUtils.isNotBlank(esType)) {
switch (esType) {
case "long":
return "Long";
case "integer":
return "Integer";
case "short":
return "Short";
case "byte":
return "Byte";
case "double":
return "Double";
case "float":
return "Float";
case "boolean":
return "Boolean";
case "date":
return "Date";
case "keyword":
case "text":
case "ip":
case "geo_shape":
case "geo_point":
return "String";
case "scaled_float":
return "BigDecimal";
case "nested":
return String.format("List<%s>", parseClassName(key));
default:
return "Object";
}
}
return "String";
}
}

View File

@ -0,0 +1,36 @@
package org.dromara.easyes.test.generated;
import lombok.Data;
import org.dromara.easyes.annotation.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@IndexName("EasyesDocument")
@Settings(shardsNum = 3, replicasNum = 2)
@Data
public class EasyesDocument {
// Fields
private String authorName;
private BigDecimal bigNum;
private Date gmtCreate;
private String caseTest;
private String creator;
private String nullField;
private String address;
private String geoLocation;
private String subTitle;
private String multiField;
private String wula;
private Integer starNum;
private String ipAddress;
private String title;
private String content;
private List<User> users;
private String filedData;
private String english;
private String commentContent;
private String location;
private Object vector;
}

View File

@ -0,0 +1,11 @@
package org.dromara.easyes.test.generated;
import lombok.Data;
import org.dromara.easyes.annotation.*;
@Data
public class Faq {
// Fields
private String answer;
private String faqName;
}

View File

@ -0,0 +1,15 @@
package org.dromara.easyes.test.generated;
import lombok.Data;
import org.dromara.easyes.annotation.*;
import java.util.List;
@Data
public class User {
// Fields
private List<Faq> faqs;
private String password;
private String userName;
private Integer age;
}

View File

@ -0,0 +1,40 @@
package org.dromara.easyes.test.generator;
import org.dromara.easyes.annotation.IndexName;
import org.dromara.easyes.core.config.GeneratorConfig;
import org.dromara.easyes.core.toolkit.Generator;
import org.dromara.easyes.test.TestEasyEsApplication;
import org.dromara.easyes.test.entity.Document;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.io.File;
/**
* entity代码生成器测试
*
* @author hwy
**/
@DisplayName("easy-es领域实体生成单元测试")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(classes = TestEasyEsApplication.class)
public class GeneratorTest {
@Resource
private Generator generator;
/**
* 测试根据已有索引生成领域模型
*/
@Test
public void testGenerate() {
IndexName indexName = Document.class.getAnnotation(IndexName.class);
GeneratorConfig config = new GeneratorConfig();
// 将生成的领域模型放置在当前项目的指的包路径下
String destPackage = "org.dromara.easyes.test.generated";
config.setDestPackage(destPackage);
config.setIndexName(indexName.value());
Boolean success = generator.generate(config);
Assertions.assertTrue(success,"generate failed!");
}
}