From 2e49638052435c2a449b82ee74e3a141ef6bff57 Mon Sep 17 00:00:00 2001 From: aoyu <1790306716@qq.com> Date: Sat, 31 Dec 2022 14:07:30 +0800 Subject: [PATCH] feat[storage] Add @ExcelFieldName to specify the column name of the resource file --- .../zfoo/storage/interpreter/ExcelReader.java | 27 +++++++++-------- .../zfoo/storage/interpreter/JsonReader.java | 6 +++- .../interpreter/ResourceInterpreter.java | 28 ++++++++++++++---- .../storage/model/anno/ExcelFieldName.java | 13 ++++++++ .../storage/model/resource/ResourceEnum.java | 2 +- .../java/com/zfoo/storage/model/vo/IdDef.java | 6 ++-- .../com/zfoo/storage/model/vo/IndexDef.java | 4 +-- .../com/zfoo/storage/model/vo/Storage.java | 12 ++++---- .../schema/StorageDefinitionParser.java | 4 +-- .../strategy/StringToClassConverter.java | 2 +- .../strategy/StringToDateConverter.java | 2 +- .../com/zfoo/storage/util/ExportUtils.java | 8 ++--- .../com/zfoo/storage/ApplicationTest.java | 2 +- .../storage/resource/StudentResource.java | 3 +- .../test/resources/excel/StudentResource.xlsx | Bin 9271 -> 9304 bytes 15 files changed, 77 insertions(+), 42 deletions(-) create mode 100644 storage/src/main/java/com/zfoo/storage/model/anno/ExcelFieldName.java diff --git a/storage/src/main/java/com/zfoo/storage/interpreter/ExcelReader.java b/storage/src/main/java/com/zfoo/storage/interpreter/ExcelReader.java index 82701969..c133f5ca 100644 --- a/storage/src/main/java/com/zfoo/storage/interpreter/ExcelReader.java +++ b/storage/src/main/java/com/zfoo/storage/interpreter/ExcelReader.java @@ -23,7 +23,9 @@ import org.apache.poi.ss.usermodel.WorkbookFactory; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.util.*; +import java.util.stream.Collectors; /** * @author meiwei666 @@ -31,14 +33,14 @@ import java.util.*; */ public abstract class ExcelReader { - public static ResourceData readResourceDataFromExcel(InputStream inputStream, String fileName) { + public static ResourceData readResourceDataFromExcel(InputStream inputStream, String resourceClassName) { // 只读取代码里写的字段 - var wb = createWorkbook(inputStream, fileName); + var wb = createWorkbook(inputStream, resourceClassName); // 默认取到第一个sheet页 var sheet = wb.getSheetAt(0); var iterator = sheet.iterator(); //设置所有列 - var headers = getHeaders(iterator, fileName); + var headers = getHeaders(iterator, resourceClassName); var rows = new ArrayList>(); while (iterator.hasNext()) { @@ -57,19 +59,19 @@ public abstract class ExcelReader { } rows.add(columns); } - return ResourceData.valueOf(fileName, headers, rows); + return ResourceData.valueOf(resourceClassName, headers, rows); } - private static List getHeaders(Iterator iterator, String fileName) { + private static List getHeaders(Iterator iterator, String resourceClassName) { // 获取配置表的有效列名称,默认第一行就是字段名称 var fieldRow = iterator.next(); if (fieldRow == null) { - throw new RunException("Failed to get attribute control column from excel file of resource [class:{}]", fileName); + throw new RunException("Failed to get attribute control column from excel file of resource [class:{}]", resourceClassName); } // 默认第二行字段类型 var typeRow = iterator.next(); if (typeRow == null) { - throw new RunException("Failed to get type control column from excel file of resource [class:{}]", fileName); + throw new RunException("Failed to get type control column from excel file of resource [class:{}]", resourceClassName); } // 默认第三行为描述,需要的时候再使用 var desRow = iterator.next(); @@ -84,19 +86,19 @@ public abstract class ExcelReader { if (Objects.isNull(typeCell)) { continue; } - var fieldName = CellUtils.getCellStringValue(fieldCell); - if (StringUtils.isEmpty(fieldName)) { + var excelFieldName = CellUtils.getCellStringValue(fieldCell); + if (StringUtils.isEmpty(excelFieldName)) { continue; } var typeName = CellUtils.getCellStringValue(typeCell); if (StringUtils.isEmpty(typeName)) { continue; } - var previousValue = cellFieldMap.put(fieldName, i); + var previousValue = cellFieldMap.put(excelFieldName, i); if (Objects.nonNull(previousValue)) { - throw new RunException("There are duplicate attribute control columns [field:{}] in the Excel file of the resource [class:{}]", fieldName,fileName); + throw new RunException("There are duplicate attribute control columns [field:{}] in the Excel file of the resource [class:{}]", excelFieldName,resourceClassName); } - headerList.add(ResourceHeader.valueOf(fieldName, typeName, i)); + headerList.add(ResourceHeader.valueOf(excelFieldName, typeName, i)); } return headerList; } @@ -108,5 +110,4 @@ public abstract class ExcelReader { throw new RunException(e, "Static resource [{}] is abnormal, and the file cannot be read", fileName); } } - } diff --git a/storage/src/main/java/com/zfoo/storage/interpreter/JsonReader.java b/storage/src/main/java/com/zfoo/storage/interpreter/JsonReader.java index 1368ba16..9058e3c4 100644 --- a/storage/src/main/java/com/zfoo/storage/interpreter/JsonReader.java +++ b/storage/src/main/java/com/zfoo/storage/interpreter/JsonReader.java @@ -28,7 +28,11 @@ public abstract class JsonReader { public static ResourceData readResourceDataFromCSV(InputStream input) { try { - return JsonUtils.string2Object(StringUtils.bytesToString(IOUtils.toByteArray(input)), ResourceData.class); + var resourceData= JsonUtils.string2Object(StringUtils.bytesToString(IOUtils.toByteArray(input)), ResourceData.class); + for(int i=0;i getFieldInfos(Map fieldMap, Class clazz) { var fieldList = ReflectionUtils.notStaticAndTransientFields(clazz); for (var field : fieldList) { - if (!fieldMap.containsKey(field.getName())) { - throw new RunException("The declaration attribute [filed:{}] of the resource class [class:{}] cannot be obtained, please check the format of the configuration table", field.getName(), clazz); - } - if (field.isAnnotationPresent(Id.class)) { var cellIndex = fieldMap.get(field.getName()); if (cellIndex != 0) { @@ -112,7 +109,26 @@ public class ResourceInterpreter { } } } - return fieldList.stream().map(it -> new FieldInfo(fieldMap.get(it.getName()), it)).collect(Collectors.toList()); + return fieldList.stream().filter(it1-> + fieldMap.keySet().stream().anyMatch(it->{ + var ans=false; + if(it1.isAnnotationPresent(ExcelFieldName.class)){ + ans|=it.equals(it1.getAnnotation(ExcelFieldName.class).value()); + } + ans|=it.equals(it1.getName()); + return ans; + })).map(it1->{ + String excelFieldName; + List list=null; + if(it1.isAnnotationPresent(ExcelFieldName.class)){ + list=fieldMap.keySet().stream().filter(it->it.equals(it1.getAnnotation(ExcelFieldName.class).value())).collect(Collectors.toList()); + } + if(list==null||list.size()==0){ + list=fieldMap.keySet().stream().filter(it->it.equals(it1.getName())).collect(Collectors.toList()); + } + excelFieldName=list.get(0); + return new FieldInfo(fieldMap.get(excelFieldName), it1); + }).collect(Collectors.toList()); } private static class FieldInfo { @@ -142,7 +158,7 @@ public class ResourceInterpreter { if (StringUtils.isEmpty(name)) { continue; } - var previousValue = cellFieldMap.put(name, i); + var previousValue = cellFieldMap.put(name, cell.getIndex()); if (Objects.nonNull(previousValue)) { throw new RunException("There are duplicate attribute control columns [field:{}] in the Excel file of the resource [class:{}]", name, clazz.getSimpleName()); } diff --git a/storage/src/main/java/com/zfoo/storage/model/anno/ExcelFieldName.java b/storage/src/main/java/com/zfoo/storage/model/anno/ExcelFieldName.java new file mode 100644 index 00000000..650f38f4 --- /dev/null +++ b/storage/src/main/java/com/zfoo/storage/model/anno/ExcelFieldName.java @@ -0,0 +1,13 @@ +package com.zfoo.storage.model.anno; + +import java.lang.annotation.*; + +/** + *指定文件列名,不指定则默认列名与字段名一致 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface ExcelFieldName { + String value(); +} diff --git a/storage/src/main/java/com/zfoo/storage/model/resource/ResourceEnum.java b/storage/src/main/java/com/zfoo/storage/model/resource/ResourceEnum.java index e203b875..b0215a66 100644 --- a/storage/src/main/java/com/zfoo/storage/model/resource/ResourceEnum.java +++ b/storage/src/main/java/com/zfoo/storage/model/resource/ResourceEnum.java @@ -42,7 +42,7 @@ public enum ResourceEnum { static { for (var resourceEnum : ResourceEnum.values()) { var previousValue = typeMap.putIfAbsent(resourceEnum.type, resourceEnum); - AssertionUtils.isNull(previousValue, "ResourceEnum中不应该含有重复type的枚举类[{}]和[{}]", resourceEnum, previousValue); + AssertionUtils.isNull(previousValue, "ResourceEnum should not contain enumeration classes [{}] and [{}] of repeated type", resourceEnum, previousValue); } } diff --git a/storage/src/main/java/com/zfoo/storage/model/vo/IdDef.java b/storage/src/main/java/com/zfoo/storage/model/vo/IdDef.java index 7a521e1d..6b7a3e54 100644 --- a/storage/src/main/java/com/zfoo/storage/model/vo/IdDef.java +++ b/storage/src/main/java/com/zfoo/storage/model/vo/IdDef.java @@ -30,13 +30,13 @@ public class IdDef { public static IdDef valueOf(Class clazz) { var fields = ReflectionUtils.getFieldsByAnnoInPOJOClass(clazz, Id.class); if (fields.length <= 0) { - throw new RunException("class[{}]没有被Id注解标识的主键(如果确实已经被Id注解标注,注意不要使用ORM的Id注解)", clazz.getName()); + throw new RunException("There is no a primary key identified by the Id annotation in class[{}](if it has indeed been annotated by the Id annotation, be careful not to use the ORM Id annotation)", clazz.getName()); } if (fields.length > 1) { - throw new RunException("类[{}]的主键Id注解重复", clazz.getName()); + throw new RunException("The primary key Id annotation of class [{}] is duplicated", clazz.getName()); } if (fields[0] == null) { - throw new RunException("不合法的Id资源映射对象:" + clazz.getName()); + throw new RunException("Illegal Id resource mapping object:" + clazz.getName()); } var idField = fields[0]; ReflectionUtils.makeAccessible(idField); diff --git a/storage/src/main/java/com/zfoo/storage/model/vo/IndexDef.java b/storage/src/main/java/com/zfoo/storage/model/vo/IndexDef.java index 02316216..1445c00e 100644 --- a/storage/src/main/java/com/zfoo/storage/model/vo/IndexDef.java +++ b/storage/src/main/java/com/zfoo/storage/model/vo/IndexDef.java @@ -48,7 +48,7 @@ public class IndexDef { var ormIndexes = ReflectionUtils.getFieldsByAnnoNameInPOJOClass(clazz, "com.zfoo.orm.model.anno.Index"); if (ArrayUtils.isNotEmpty(ormIndexes)) { - throw new RunException("在Storage中只能使用Storage的Index注解,不能使用Orm的Index注解,为了避免不必要的误解和增强项目的健壮性,禁止这样使用"); + throw new RunException("Only the Index annotation of Storage can be used, and the Index annotation of Orm cannot be used in Storage. In order to avoid unnecessary misunderstanding and enhance the robustness of the project, such use is prohibited"); } for (var field : fields) { @@ -60,7 +60,7 @@ public class IndexDef { for (var index : indexes) { var indexName = index.field.getName(); if (result.put(indexName, index) != null) { - throw new RuntimeException(StringUtils.format("资源类[{}]索引名称重复,索引名[}|]", clazz.getName(), indexName)); + throw new RuntimeException(StringUtils.format("The index name[{}] of resource class [{}] is duplicated.", indexName, clazz.getName())); } } diff --git a/storage/src/main/java/com/zfoo/storage/model/vo/Storage.java b/storage/src/main/java/com/zfoo/storage/model/vo/Storage.java index c310f26c..2ba937a8 100644 --- a/storage/src/main/java/com/zfoo/storage/model/vo/Storage.java +++ b/storage/src/main/java/com/zfoo/storage/model/vo/Storage.java @@ -101,13 +101,13 @@ public class Storage { public V get(K id) { V result = dataMap.get(id); - AssertionUtils.notNull(result, "静态资源[resource:{}]中表示为[id:{}]的静态资源不存在", clazz.getSimpleName(), id); + AssertionUtils.notNull(result, "The static resource represented as [id:{}] in the static resource [resource:{}] does not exist", id, clazz.getSimpleName()); return result; } public List getIndex(String indexName, Object key) { var indexValues = indexMap.get(indexName); - AssertionUtils.notNull(indexValues, "静态资源[resource:{}]不存在为[indexName:{}]的索引", clazz.getSimpleName(), indexName); + AssertionUtils.notNull(indexValues, "The index of [indexName:{}] does not exist in the static resource [resource:{}]", indexName, clazz.getSimpleName()); var values = indexValues.get(key); if (CollectionUtils.isEmpty(values)) { return Collections.emptyList(); @@ -118,7 +118,7 @@ public class Storage { @Nullable public V getUniqueIndex(String uniqueIndexName, Object key) { var indexValueMap = uniqueIndexMap.get(uniqueIndexName); - AssertionUtils.notNull(indexValueMap, "静态资源[resource:{}]不存在为[uniqueIndexName:{}]的唯一索引", clazz.getSimpleName(), uniqueIndexName); + AssertionUtils.notNull(indexValueMap, "There is no a unique index for [uniqueIndexName:{}] in the static resource [resource:{}]", uniqueIndexName, clazz.getSimpleName()); var value = indexValueMap.get(key); return value; } @@ -128,11 +128,11 @@ public class Storage { var key = (K) ReflectionUtils.getField(idDef.getField(), value); if (key == null) { - throw new RuntimeException("静态资源存在id未配置的项"); + throw new RuntimeException("There is an item with an unconfigured id in the static resource"); } if (dataMap.containsKey(key)) { - throw new RuntimeException(StringUtils.format("静态资源[resource:{}]的[id:{}]重复", clazz.getSimpleName(), key)); + throw new RuntimeException(StringUtils.format("Duplicate [id:{}] of static resource [resource:{}]", key, clazz.getSimpleName())); } // 添加资源 @@ -146,7 +146,7 @@ public class Storage { if (def.isUnique()) {// 唯一索引 var index = uniqueIndexMap.computeIfAbsent(indexKey, k -> new HashMap<>()); if (index.put(indexValue, value) != null) { - throw new RuntimeException(StringUtils.format("静态资源[class:{}]的唯一索引重复[index:{}][value:{}]", clazz.getName(), indexKey, indexValue)); + throw new RuntimeException(StringUtils.format("Duplicate unique index [index:{}][value:{}] of static resource [class:{}]", indexKey, indexValue, clazz.getName())); } } else {// 不是唯一索引 var index = indexMap.computeIfAbsent(indexKey, k -> new HashMap<>()); diff --git a/storage/src/main/java/com/zfoo/storage/schema/StorageDefinitionParser.java b/storage/src/main/java/com/zfoo/storage/schema/StorageDefinitionParser.java index df4377e0..1a6b8c6e 100644 --- a/storage/src/main/java/com/zfoo/storage/schema/StorageDefinitionParser.java +++ b/storage/src/main/java/com/zfoo/storage/schema/StorageDefinitionParser.java @@ -58,11 +58,11 @@ public class StorageDefinitionParser implements BeanDefinitionParser { var scanElement = DomUtils.getFirstChildElementByTagName(element, "scan"); if (scanElement == null) { - throw new RuntimeException("XML文件缺少[scan]元素定义"); + throw new RuntimeException("The XML file is missing a [scan] element definition"); } var resourceElement = DomUtils.getFirstChildElementByTagName(element, "resource"); if (resourceElement == null) { - throw new RuntimeException("XML文件缺少[resource]元素定义"); + throw new RuntimeException("The XML file is missing a [resource] element definition"); } resolvePlaceholder("id", "id", builder, element, parserContext); diff --git a/storage/src/main/java/com/zfoo/storage/strategy/StringToClassConverter.java b/storage/src/main/java/com/zfoo/storage/strategy/StringToClassConverter.java index 7d41dbab..9e7a657d 100644 --- a/storage/src/main/java/com/zfoo/storage/strategy/StringToClassConverter.java +++ b/storage/src/main/java/com/zfoo/storage/strategy/StringToClassConverter.java @@ -33,7 +33,7 @@ public class StringToClassConverter implements Converter> { try { return Class.forName(source, true, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException e) { - throw new IllegalArgumentException(StringUtils.format("无法将字符串[{}]转换为Class对象", source)); + throw new IllegalArgumentException(StringUtils.format("Unable to convert string [{}] to Class object", source)); } } diff --git a/storage/src/main/java/com/zfoo/storage/strategy/StringToDateConverter.java b/storage/src/main/java/com/zfoo/storage/strategy/StringToDateConverter.java index f1b332e0..50a4a6af 100644 --- a/storage/src/main/java/com/zfoo/storage/strategy/StringToDateConverter.java +++ b/storage/src/main/java/com/zfoo/storage/strategy/StringToDateConverter.java @@ -34,7 +34,7 @@ public class StringToDateConverter implements Converter { try { return df.parse(source); } catch (ParseException e) { - throw new IllegalArgumentException(StringUtils.format("字符串[{}]不符合格式要求:[yyyy-MM-dd HH:mm:ss]", source)); + throw new IllegalArgumentException(StringUtils.format("The string [{}] does not meet the format requirements: [yyyy-MM-dd HH:mm:ss]", source)); } } } diff --git a/storage/src/main/java/com/zfoo/storage/util/ExportUtils.java b/storage/src/main/java/com/zfoo/storage/util/ExportUtils.java index 4843adb7..25650492 100644 --- a/storage/src/main/java/com/zfoo/storage/util/ExportUtils.java +++ b/storage/src/main/java/com/zfoo/storage/util/ExportUtils.java @@ -104,7 +104,7 @@ public abstract class ExportUtils { for (var field : filedList) { var fieldType = field.getType(); if (!fieldType.equals(Map.class)) { - throw new RunException("[class:{}]类型声明不正确,导出的storage必须为Map接口类型", clazz.getCanonicalName()); + throw new RunException("[class:{}] type declaration is incorrect, the exported storage must be a Map interface type", clazz.getCanonicalName()); } var type = field.getGenericType(); @@ -121,13 +121,13 @@ public abstract class ExportUtils { } else if (idFieldType.equals(short.class) && keyType.equals(Short.class)) { } else if (idFieldType.equals(byte.class) && keyType.equals(Byte.class)) { } else { - throw new RunException("[class:{}]中的[field:{}]类型声明不正确,类型需要改为[Map<{}, {}>]", clazz.getSimpleName(), field.getName(), idFieldType, valueClass.getSimpleName()); + throw new RunException("The [field:{}] type declaration in [class:{}] is incorrect, the type needs to be changed to [Map<{}, {}>]", field.getName(), clazz.getSimpleName(), idFieldType, valueClass.getSimpleName()); } } else if (!keyType.equals(idFieldType)) { - throw new RunException("[class:{}]中的[field:{}]类型声明不正确,类型需要改为[Map<{}, {}>]", clazz.getSimpleName(), field.getName(), idFieldType, valueClass.getSimpleName()); + throw new RunException("The [field:{}] type declaration in [class:{}] is incorrect, the type needs to be changed to [Map<{}, {}>]", field.getName(), clazz.getSimpleName(), idFieldType, valueClass.getSimpleName()); } if (!wrapClass.add(valueClass)) { - throw new RunException("[class:{}]中含有重复的类型[{}]", clazz.getCanonicalName(), valueType); + throw new RunException("[class:{}] contains duplicate type [{}]", clazz.getCanonicalName(), valueType); } ReflectionUtils.setField(field, instance, storage.getData()); } diff --git a/storage/src/test/java/com/zfoo/storage/ApplicationTest.java b/storage/src/test/java/com/zfoo/storage/ApplicationTest.java index 21611487..919f0b00 100644 --- a/storage/src/test/java/com/zfoo/storage/ApplicationTest.java +++ b/storage/src/test/java/com/zfoo/storage/ApplicationTest.java @@ -49,7 +49,7 @@ public class ApplicationTest { var studentManager = context.getBean(StudentManager.class); var studentResources = studentManager.studentResources; var studentCsvResources = studentManager.studentCsvResources; - // @Resource注解没指定别名,类名称和Excel名称必须完全一致,Excel的列名称必须对应对象的属性名称 + // @Resource注解没指定别名,类名称和Excel名称必须完全一致,没有使用@ExcelFieldName对象属性名会自动对应同名的资源文件列名 for (StudentResource resource : studentResources.getAll()) { logger.info(JsonUtils.object2String(resource)); } diff --git a/storage/src/test/java/com/zfoo/storage/resource/StudentResource.java b/storage/src/test/java/com/zfoo/storage/resource/StudentResource.java index fcc5db35..38b12221 100644 --- a/storage/src/test/java/com/zfoo/storage/resource/StudentResource.java +++ b/storage/src/test/java/com/zfoo/storage/resource/StudentResource.java @@ -13,6 +13,7 @@ package com.zfoo.storage.resource; +import com.zfoo.storage.model.anno.ExcelFieldName; import com.zfoo.storage.model.anno.Id; import com.zfoo.storage.model.anno.Index; import com.zfoo.storage.model.anno.Resource; @@ -35,13 +36,13 @@ public class StudentResource { @Index private String name; + @ExcelFieldName("年龄") private int age; private float score; private String[] courses; private User[] users; private List userList; private User user; - /** * 不想映射的字段必须加上transient关键字,这样就不会从Excel中去找对应的列 */ diff --git a/storage/src/test/resources/excel/StudentResource.xlsx b/storage/src/test/resources/excel/StudentResource.xlsx index c0a744a4a6797a6f57e4dac5fd5e6548b961bf6e..eed0851e27aa35da10c15287d7f423e9df60388c 100644 GIT binary patch delta 5325 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 E13B=?Z~y=R delta 5362 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