feat: 新增文件导入/导出

This commit is contained in:
zengxiaobo 2024-07-05 16:17:28 +08:00
parent c5e235872b
commit d4f01bb227
8 changed files with 2690 additions and 0 deletions

View File

@ -0,0 +1,18 @@
package cn.axzo.foundation.page;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageReq implements IPageReq{
Integer page;
Integer pageSize;
List<String> sort;
}

44
excel-support-lib/pom.xml Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.axzo.foundation</groupId>
<artifactId>axzo-lib-box</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<groupId>cn.axzo.maokai</groupId>
<artifactId>excel-support-lib</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>cn.axzo.foundation</groupId>
<artifactId>web-support-lib</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,918 @@
package cn.axzo.foundation.excel.support;
import cn.axzo.foundation.exception.BusinessException;
import cn.axzo.foundation.page.IPageReq;
import cn.axzo.foundation.page.PageResp;
import cn.axzo.foundation.util.VarParamFormatter;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.util.TypeUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import lombok.*;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.poi.ss.SpreadsheetVersion;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static cn.axzo.foundation.excel.support.DataSheetClient.CellMeta.*;
/**
* 用于发送sms & email验证码, 或者获取图片验证码. 并提供校验验证码是否正确的能力
*/
public interface DataSheetClient {
ExporterBuilder exportBuilder();
/**
* 数据导入的builder
*
* @return
*/
ImporterBuilder importBuilder();
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(fluent = true)
abstract class ImporterBuilder {
/**
* 导入的场景名称, 必填
*/
@NonNull
private String scene;
/**
* 导入格式
*/
@NonNull
private ImportFormat format;
/**
* EXCEL格式的sheetName
*/
private String tableName;
private Meta meta;
/**
* 是否忽略导入文件表头的原meta
* 默认按false处理{@link #meta}为空时尝试使用原文件表头meta
*/
private Boolean ignoreHeaderMeta;
private boolean debugEnabled;
/**
* 忽略未知的列(cellMeta中未定义的列)
*/
private boolean ignoreUnknownColumn;
/**
* 是否自动清除表格内容的前后空白字符
* 默认true即会清除前后空白字符
*/
private Boolean autoTrim;
/**
* 解析结果ImportResp中的lines是否包含解析失败的行
* true: 解析失败的时候不会抛异常会在每行的JSONObject中添加
* "errors":[{"columnIndex":1,"columnKey":"","columnName":"","rawValue":"","errorCode":"","errorMsg":""}]
* false: 解析失败的时候抛第一个异常
*/
private boolean includeLineErrors;
/**
* 允许最大的导入行数
*/
private Integer allowMaxLineCount;
/** 将excel中的表头转换为cellMeta中的表头 */
private Function<String, String> headerConverter;
/**
* 操作人, 可选用于导入统计报告
*/
private String operator;
public abstract Importer build();
}
interface Importer {
/**
* 所有字段读取为String类型
*
* @param inputStream
* @return
*/
ImportResp<JSONObject> readAll(InputStream inputStream);
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ImportResp<T> {
/**
* 导入的场景名称
*/
private String scene;
/**
* 模版对应的code以及版本
*/
private String templateCode;
private String version;
/**
* headers
*/
private List<String> headers;
/**
* 每一行的数据
*/
private List<T> lines;
/**
* header的行数
*/
private Integer headerRowCount;
/**
* 导入数据总行数, 不包含header
*/
private Integer rowCount;
/**
* 导入数据列数
*/
private Integer columnCount;
/**
* Meta数据
*/
private Meta meta;
/**
* 消耗的时间
*/
private Long elapsedMillis;
/**
* 操作人
*/
private String operator;
}
@AllArgsConstructor
@Getter
enum ImportFormat {
EXCEL("xlsx"),
// TODO: 支持其他类型
// CSV("csv")
;
private String suffix;
}
@Data
@NoArgsConstructor
@Accessors(fluent = true)
abstract class ExporterBuilder {
/**
* 导出的场景名称, 必填
*/
@NonNull
private String scene;
/**
* 导出格式
*/
@NonNull
private ExportFormat format;
/**
* 顶部提示内容主要用于一些提示场景如导出数据超范围等文案提示等支持多行
*/
private Function<PageResp<JSONObject>, List<String>> topHintsSupplier;
/**
* 提供分页获取数据的方法
*/
@NonNull
private Function<IPageReq, PageResp<JSONObject>> rowSupplier;
/**
* 导出的总数据量限制不指定时按照分页返回的结果全量导出
*/
private Long limit = Long.MAX_VALUE;
/**
* 获取数据的分页大小, 默认每页200条(MybatisPlus的PaginationInterceptor默认limit为500)
*/
private Long pageSize = 200L;
/**
* 表名仅当导出格式为excel时有效
*/
private String sheetName;
@Deprecated
public ExporterBuilder tableName(String tableName) {
this.sheetName = tableName;
return this;
}
/**
* 列映射表, key=列名 value=获取字段值的方法, 方法参数1为当前row的JSONObject, 参数2为当前row的index返回列字段值
*/
private ImmutableMap<String, BiFunction<JSONObject, Integer, Object>> columnMap;
/**
* 多行表头映射表, 当表头由多行数据组成时使用. key=表头, value=该表头对应的每行的内容
*/
private ImmutableMap<String, List<String>> multiLineHeadMap;
/**
* 行数据的转换器外部可以通过它将原始的行数据根据需要做转换
*/
private Function<JSONObject, List<JSONObject>> rowConverter;
/**
* 页数据的转换器外部可以通过它将也的数据根据需要做转换
* 它会在 rowconvert() 执行完成
*/
private Function<List<JSONObject>, List<JSONObject>> pageConverter;
private BiConsumer<Integer, String> onProgress;
/**
* 批注信息可选
*/
private Meta meta;
/**
* 是否开启调试日志
*/
private Boolean debugEnabled = true;
/**
* 导出的fields. 优先使用fields来创建column&meta
*/
private List<ExportField> fields;
/**
* meta的version
*/
private String version;
/** 文件名 */
private String fileName;
/** 操作人, 可选,用于异步导出 */
private String operator;
/** 拓展字段可用于存放请求参数等该字段会回传给FileLoaderParam */
private JSONObject ext;
/**
* 仅对导出excel生效是否自动清除表格内容的前后空白字符
* 默认true即会清除前后空白字符
*/
private Boolean autoTrim;
abstract public Exporter build();
}
@Data
class ExportField {
private String column;
private String header;
private List<String> headerLines;
private Integer width;
private CellMeta.Type type;
private Boolean mandatory;
private Boolean wrapText;
private String lowerType;
private String upperType;
private String dateTimeFormat;
private List<Object> options;
private BiFunction<JSONObject, Integer, Object> reader;
@Builder
public ExportField(String column, String header, List<String> headerLines, Integer width, CellMeta.Type type,
Boolean mandatory, Boolean wrapText, Function<String, Object> resultConverter,
BiFunction<JSONObject, Integer, Object> reader, List<Object> options) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(column), "column不能为空");
Preconditions.checkArgument(!Strings.isNullOrEmpty(header), "header不能为空");
Preconditions.checkArgument(!(resultConverter != null && reader != null), "reader & resultConverter不能同时存在");
this.column = column;
this.header = header;
this.headerLines = headerLines;
this.width = width;
this.type = Optional.ofNullable(type).orElse(CellMeta.Type.STRING);
this.mandatory = Optional.ofNullable(mandatory).orElse(Boolean.FALSE);
this.wrapText = wrapText;
if (reader != null) {
this.reader = reader;
} else {
this.reader = Optional.ofNullable(resultConverter)
.map(e -> (BiFunction<JSONObject, Integer, Object>) (row, integer) -> e.apply(Strings.nullToEmpty(row.getString(column))))
.orElseGet(() -> DataSheetClient.stringCellReader(column));
}
this.options = options;
}
public CellMeta toCellMeta() {
JSONObject params = new JSONObject();
if (CellMeta.Type.RANGE == type) {
params.put(EXT_KEY_RANGE_LOWER_TYPE, lowerType);
params.put(EXT_KEY_RANGE_UPPER_TYPE, upperType);
}
if (CellMeta.Type.DATE == type || CellMeta.Type.DATETIME == type) {
params.put(EXT_KEY_DATETIME_FORMAT, dateTimeFormat);
}
return CellMeta.builder()
.key(column).name(header).width(width).mandatory(mandatory).type(type).wrapText(wrapText)
.params(params).options(options).build();
}
}
interface Exporter {
/**
* 导出到OutputStream中
*
* @param outputStream
* @return
*/
ExportResp export(@NonNull OutputStream outputStream);
/**
* 导出到HttpServletResponse中
*
* @param response
* @return
*/
ExportResp export(@NonNull HttpServletResponse response);
/**
* 导出到HttpServletResponse中可指定导出的文件名
*
* @param response
* @return
*/
ExportResp export(HttpServletResponse response, String filename);
}
interface AsyncExporter {
/**
* 异步导出数据
*
* @param dataSheetExporter 实现导出逻辑的 exporter
* @return batchId
*/
String export(@NonNull ExporterBuilder dataSheetExporter);
/**
* 多表单数据异步导出至同一文件中, 仅支持导出格式是EXCEL
*
* @param fileName 导出文件名
* @param dataSheetExporters 实现导出逻辑的 exporter 列表
* @return batchId
*/
String export(String fileName, @NonNull List<ExporterBuilder> dataSheetExporters);
Progress getProgress(@NonNull String batchId);
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
class Progress {
String fileName;
int progress;
String errorMsg;
String downloadUrl;
public boolean isSuccess() {
return Strings.isNullOrEmpty(errorMsg);
}
public boolean isDone() {
return progress < 0 || progress >= 100;
}
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class ExportResp {
/**
* 导出场景
*/
private String scene;
/**
* 导出数据条数
*/
private Long exportedCount;
/**
* 导出数据总行数包含了顶部提示行及表头行
*/
private Long rowCount;
/**
* 导出数据列数
*/
private Long columnCount;
/**
* 导出数据行数
*/
private Long dataRowCount;
/**
* 耗时
*/
private Long elapsedMillis;
/**
* 操作人
*/
private String operator;
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class Meta {
public static final String PATTERN_VERSION_OPEN = "$V{";
public static final String PATTERN_CELL_OPEN = "$C{";
public static final String PATTERN_IGNORE_OPEN = "$I{";
public static final String PATTERN_EXTRA_OPEN = "$E{";
public static final String PATTERN_CLOSE = "}";
public static final String RANGE_TYPE_OPEN_OPEN = "1";
public static final String RANGE_TYPE_OPEN_CLOSED = "2";
public static final String RANGE_TYPE_CLOSED_OPEN = "3";
public static final String RANGE_TYPE_CLOSED_CLOSED = "4";
public static final String IGNORE_ROW_KEY = "r";
public static final String IGNORE_COLUMN_KEY = "c";
public static final String IGNORE_ROW_AND_COLUMN_KEY = "b";
private String templateCode;
private String version;
private List<CellMeta> cellMetas;
/**
* 忽略掉的行号
*/
private List<Integer> ignoreRowIndexes;
/**
* 忽略掉的列号
*/
private List<Integer> ignoreColumnIndexes;
/**
* 忽略掉的列名
* 当导入文件中有meta信息时该ignore信息将会丢失
*/
private Set<String> ignoreColumnNames;
public List<Integer> getIgnoreRowIndexes() {
return Optional.ofNullable(ignoreRowIndexes).orElseGet(ImmutableList::of);
}
public List<Integer> getIgnoreColumnIndexes() {
return Optional.ofNullable(ignoreColumnIndexes).orElseGet(ImmutableList::of);
}
public Set<String> getIgnoreColumnNames() {
return Optional.ofNullable(ignoreColumnNames).orElseGet(ImmutableSet::of);
}
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class CellMeta {
public static final String EXT_KEY_DATETIME_FORMAT = "dateTimeFormat";
public static final String EXT_KEY_RANGE_LOWER_TYPE = "lowerType";
public static final String EXT_KEY_RANGE_UPPER_TYPE = "upperType";
public static final String RANGE_BOUND_TYPE_OPEN = "open";
public static final String RANGE_BOUND_TYPE_CLOSED = "closed";
public static final Set<String> RANGE_TYPES = ImmutableSet.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_CLOSED);
private static final List<String> DEFAULT_DATE_FORMATS = ImmutableList.of(
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
"yyyy.MM.dd",
"yyyy.MM.dd HH:mm",
"yyyy.MM.dd HH:mm:ss",
"yyyy年MM月dd日",
"yyyy年MM月dd日 HH:mm",
"yyyy年MM月dd日 HH:mm:ss",
"yyyy年MM月dd日 HH时mm分",
"yyyy年MM月dd日 HH时mm分ss秒",
"yyyy/MM/dd",
"yyyy/MM/dd HH:mm",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ssz",
"yyyy-MM-dd'T'HH:mm:ss",
"MM/dd/yyyy HH:mm:ss a",
"yyyyMMddHHmmss",
"yyyyMMdd"
);
private static final Map<String, Boolean> BOOLEAN_MAP = ImmutableMap.<String, Boolean>builder()
.put("", Boolean.TRUE)
.put("yes", Boolean.TRUE)
.put("y", Boolean.TRUE)
.put("1", Boolean.TRUE)
.put("", Boolean.FALSE)
.put("no", Boolean.FALSE)
.put("n", Boolean.FALSE)
.put("0", Boolean.FALSE)
.build();
/**
* cell的key
*/
private String key;
/**
* cell的header一般为中文
*/
private String name;
/**
* cell的宽度
*/
private Integer width;
/**
* 类型
*/
private Type type;
/**
* 是否必需, 为false时, 允许该列在Excel中不存在
*/
private Boolean mandatory;
/**
* 可选值
*/
private List<Object> options;
/**
* 存放一些和type相关的参数例如DateTime的pattern格式Range的开闭区间
*/
private JSONObject params;
/**
* 存放一些额外的信息
*/
private JSONObject ext;
/**
* wrap the text automatically
*/
private Boolean wrapText;
/**
* 导入的时候将原始的String转换为特定的类型
*/
private Function<String, Object> importConverter;
/**
* 字段值额外的校验方法支持根据当前字段值及整行数据进行自定义校验
*/
private BiConsumer<Object, JSONObject> cellValidator;
public void validate() {
if (type == Type.RANGE
&& (!RANGE_TYPES.contains(params.getString(EXT_KEY_RANGE_LOWER_TYPE))
|| !RANGE_TYPES.contains(params.getString(EXT_KEY_RANGE_UPPER_TYPE)))) {
throw ResultCode.IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR.toException();
}
}
public Object convertType(String value) {
if (importConverter != null) {
return importConverter.apply(value);
}
if (Boolean.TRUE.equals(mandatory) && Strings.isNullOrEmpty(value)) {
throw ResultCode.IMPORT_CELL_META_MISSING_MANDATORY_VALUE.toException();
}
// 非必填的字段如果为空直接返回
if (value == null) {
return type == Type.STRING ? StringUtils.EMPTY : null;
}
switch (type) {
case INT:
// 移除千分符
return Integer.valueOf(value.replace(",", ""));
case LONG:
// 移除千分符
return Long.valueOf(value.replace(",", ""));
case FLOAT:
// 移除千分符
return Float.valueOf(value.replace(",", ""));
case DOUBLE:
// 移除千分符
return Double.valueOf(value.replace(",", ""));
case BIG_DECIMAL:
// 移除千分符
return new BigDecimal(value.replace(",", ""));
case DATE:
case DATETIME:
return parseDate(value, DEFAULT_DATE_FORMATS);
case RANGE:
// 格式 100-200
List<Integer> values = Splitter.on("-").omitEmptyStrings().trimResults().splitToList(value)
.stream()
.map(Integer::valueOf)
.collect(Collectors.toList());
if (values.size() != 2) {
throw ResultCode.IMPORT_CELL_RANGE_FORMAT_ERROR.toException();
}
if (values.get(0) >= values.get(1)) {
throw ResultCode.IMPORT_CELL_RANGE_VALUE_ERROR.toException();
}
return new JSONObject()
.fluentPut("lower", values.get(0))
.fluentPut("upper", values.get(1))
.fluentPut(EXT_KEY_RANGE_LOWER_TYPE, params.getString(EXT_KEY_RANGE_LOWER_TYPE))
.fluentPut(EXT_KEY_RANGE_UPPER_TYPE, params.getString(EXT_KEY_RANGE_UPPER_TYPE));
case BOOLEAN:
return Optional.ofNullable(BOOLEAN_MAP.get(value.toLowerCase()))
.orElseThrow(ResultCode.IMPORT_CELL_BOOLEAN_VALUE_ERROR::toException);
default:
return Strings.nullToEmpty(value);
}
}
private LocalDateTime parseDate(String dateStr, List<String> formats) {
try {
Date date = DateUtils.parseDate(dateStr, formats.toArray(new String[0]));
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
} catch (Exception e) {
throw ResultCode.IMPORT_CELL_DATETIME_CONVERT_FAILED.toException("不支持的日期格式{}", dateStr);
}
}
@AllArgsConstructor
public enum Type {
INT("00"),
LONG("01"),
FLOAT("02"),
DOUBLE("03"),
STRING("04"),
BIG_DECIMAL("05"),
DATE("06"),
DATETIME("07"),
RANGE("08"),
BOOLEAN("09");
@Getter
private String code;
private static Map<String, Type> map = Stream.of(Type.values())
.collect(Collectors.toMap(Type::getCode, Function.identity()));
public static Type from(String code) {
return map.get(code);
}
}
}
@AllArgsConstructor
@Getter
enum ExportFormat {
EXCEL("application/vnd.ms-excel", ".xlsx"),
CSV("text/csv", ".csv");
private String contentType;
private String suffix;
}
static BiFunction<JSONObject, Integer, Object> indexCellReader() {
return (row, index) -> String.valueOf(index + 1);
}
static BiFunction<JSONObject, Integer, Object> stringCellReader(String columnName) {
// excel库poi会检查文本格式的单元格内容当超过32767时将会抛出IllegalArgumentException异常
// 参考 {@link org.apache.poi.xssf.streaming.SXSSFCell.setCellValue}
return (row, index) -> StringUtils.left(Strings.nullToEmpty(row.getString(columnName)),
SpreadsheetVersion.EXCEL2007.getMaxTextLength());
}
static BiFunction<JSONObject, Integer, Object> dateTimeCellReader(String columnName, String pattern) {
return (row, index) -> {
Long epochMillis = row.getLong(columnName);
if (epochMillis == null) {
return StringUtils.EMPTY;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
Instant instant = Instant.ofEpochMilli(epochMillis);
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
return formatter.format(localDateTime);
};
}
static BiFunction<JSONObject, Integer, Object> bigDecimalCellReader(String columnName) {
return bigDecimalCellReader(columnName, 2);
}
static BiFunction<JSONObject, Integer, Object> bigDecimalCellReader(String columnName, int scale) {
return (row, index) -> {
BigDecimal number = row.getBigDecimal(columnName);
if (number == null) {
number = BigDecimal.ZERO;
}
return number.stripTrailingZeros().setScale(scale, RoundingMode.HALF_UP).toPlainString();
};
}
static BiFunction<JSONObject, Integer, Object> jsonPathCellReader(String jsonPath) {
return (row, index) -> {
Object obj = JSONPath.eval(row, jsonPath);
/**
* excel单元格对于数值最大只能保有15位超过15位的数值(Long)会被置为0比如在导出uid的场景会导致数据错误
* https://docs.microsoft.com/en-us/office/troubleshoot/excel/last-digits-changed-to-zeros
* https://wenku.baidu.com/view/e1d2a497f624ccbff121dd36a32d7375a417c6ad.html
*/
if (obj instanceof Long && obj.toString().length() > 15) {
obj = obj.toString();
}
if (obj instanceof String) {
// excel库poi会检查文本格式的单元格内容当超过32767时将会抛出IllegalArgumentException异常
// 参考 {@link org.apache.poi.xssf.streaming.SXSSFCell.setCellValue}
return StringUtils.left(obj.toString(), SpreadsheetVersion.EXCEL2007.getMaxTextLength());
}
return obj;
};
}
static BiFunction<JSONObject, Integer, Object> jsonPathDateTimeCellReader(String jsonPath, String pattern) {
return (row, index) -> {
Object val = JSONPath.eval(row, jsonPath);
Long epochMillis = TypeUtils.cast(val, Long.class, ParserConfig.getGlobalInstance());
if (epochMillis == null) {
return StringUtils.EMPTY;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
Instant instant = Instant.ofEpochMilli(epochMillis);
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
return formatter.format(localDateTime);
};
}
static BiFunction<JSONObject, Integer, Object> jsonPathBigDecimalCellReader(String jsonPath) {
return jsonPathBigDecimalCellReader(jsonPath, 2);
}
static BiFunction<JSONObject, Integer, Object> jsonPathBigDecimalCellReader(String jsonPath, int scale) {
return (row, index) -> {
Object val = JSONPath.eval(row, jsonPath);
if (val == null) {
val = BigDecimal.ZERO;
}
BigDecimal bigDecimal = TypeUtils.cast(val, BigDecimal.class, ParserConfig.getGlobalInstance());
return bigDecimal.stripTrailingZeros().setScale(scale, RoundingMode.HALF_UP).toPlainString();
};
}
@Getter
class DataSheetException extends BusinessException {
private String subErrorCode;
/** 异常相关的行号列号0开始 */
private Integer rowIndex;
private Integer columnIndex;
/** 异常相关的列名称,中文 */
private List<String> columnNames;
public DataSheetException(ResultCode resultCode) {
super(cn.axzo.foundation.result.ResultCode.INVALID_PARAMS.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
}
public DataSheetException(ResultCode resultCode, Integer rowIndex, Integer columnIndex) {
super(cn.axzo.foundation.result.ResultCode.INVALID_PARAMS.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
}
public DataSheetException(ResultCode resultCode, List<String> columnNames) {
super(cn.axzo.foundation.result.ResultCode.INVALID_PARAMS.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
this.columnNames = columnNames;
}
public DataSheetException(ResultCode resultCode, String message) {
super(cn.axzo.foundation.result.ResultCode.INVALID_PARAMS.getErrorCode(), message);
this.subErrorCode = resultCode.getSubBizCode();
}
public DataSheetException(String subErrorCode, String errorMsg, Integer rowIndex, Integer columnIndex) {
super(cn.axzo.foundation.result.ResultCode.INVALID_PARAMS.getErrorCode(), errorMsg);
this.subErrorCode = subErrorCode;
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
}
public DataSheetException(String errorCode, String subErrorCode, String errorMsg) {
super(errorCode, errorMsg);
this.subErrorCode = subErrorCode;
}
}
@AllArgsConstructor
@Getter
enum ResultCode {
/** 解析Excel的批注时相关的errorCode */
IMPORT_PARSE_MISSING_VERSION("C00", "批注中没有找到版本信息"),
IMPORT_PARSE_VERSION_FORMAT_ERROR("C01", "批注中的版本格式不对"),
IMPORT_PARSE_CELL_META_MISSING_TYPE("C02", "批注中的字段没有找到类型信息"),
IMPORT_PARSE_CELL_META_FORMAT_ERROR("C03", "批注中的字段格式不对"),
IMPORT_PARSE_CELL_META_RANGE_FORMAT_ERROR("C04", "批注中的范围类型字段的格式不对"),
IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR("C05", "批注中的范围类型字段的区间类型不对"),
/** 导入数据时相关的errorCode */
IMPORT_LINES_REACHED_LIMIT("C20", "导入的数据超过了最大行数"),
IMPORT_COLUMN_RANGE_MISSING_TYPES("C21", "范围类型缺少区间类型的定义:开区间还是闭区间"),
IMPORT_COLUMN_DUPLICATED_NAME("C22", "列的名称不能重复"),
IMPORT_COLUMN_MISSING_CELL_META("C23", "字段缺少类型定义"),
IMPORT_COLUMN_NAME_NOT_MATCHED("C24", "字段的名称与类型定义的名称不一致"),
IMPORT_CELL_RANGE_FORMAT_ERROR("C25", "范围类型的格式不对"),
IMPORT_CELL_RANGE_VALUE_ERROR("C26", "范围类型的下限值必须小于上限值"),
IMPORT_CELL_DATETIME_CONVERT_FAILED("C27", "时间类型解析失败"),
IMPORT_CELL_CONVERT_FAILED("C28", "类型转换失败"),
IMPORT_CELL_META_MISSING_MANDATORY_VALUE("C29", "必填字段不能为空"),
IMPORT_CELL_BOOLEAN_VALUE_ERROR("C30", "布尔类型的值不支持"),
;
private String subBizCode;
private String message;
public DataSheetException toException() {
return new DataSheetException(this);
}
public DataSheetException toException(String message) {
return new DataSheetException(this, message);
}
public DataSheetException toException(List<String> columnNames) {
return new DataSheetException(this, columnNames);
}
public DataSheetException toException(Integer rowIndex, Integer columnIndex) {
return new DataSheetException(this, rowIndex, columnIndex);
}
public DataSheetException toException(String customMsg, Object... objects) {
if (objects == null) {
return toException(customMsg);
}
String msg = VarParamFormatter.format(customMsg, objects);
//如果最后一个参数是Throwable. 则将SimpleName附加到msg中
if (objects[objects.length - 1] instanceof Throwable) {
Throwable throwable = (Throwable) objects[objects.length - 1];
msg = String.format("%s (%s)", msg, throwable.getClass().getSimpleName());
}
return toException(msg);
}
}
}

View File

@ -0,0 +1,326 @@
package cn.axzo.foundation.excel.support.impl;
import cn.axzo.foundation.enums.AppEnvEnum;
import cn.axzo.foundation.excel.support.DataSheetClient;
import cn.axzo.foundation.util.UUIDBuilder;
import cn.axzo.foundation.web.support.AppRuntime;
import cn.axzo.foundation.web.support.utils.KeyBuilder;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.IoUtils;
import com.alibaba.excel.util.StringUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.io.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 异步数据导出.
* 将导出的进度防止redis 当导出完成后将文件通过 fileUploader 上传到云存储
*/
@Slf4j
public class DataSheetAsyncExporter implements DataSheetClient.AsyncExporter {
private static final int PROGRESS_EXPIRE_DAYS = 3;
private RedisTemplate<String, String> redisTemplate;
private AppRuntime appRuntime;
private Function<FileUploadParam, FileUploadResult> fileUploader;
private TaskExecutor executor;
public DataSheetAsyncExporter(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull AppRuntime appRuntime,
@NonNull Function<FileUploadParam, FileUploadResult> fileUploader,
@NonNull TaskExecutor executor) {
this.redisTemplate = redisTemplate;
this.appRuntime = appRuntime;
this.fileUploader = fileUploader;
this.executor = executor;
}
@Data
@lombok.Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FileUploadParam {
private byte[] bytes;
private String fileName;
private String operator;
private JSONObject ext;
}
@Data
@lombok.Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FileUploadResult {
private String downloadUrl;
}
@Override
public String export(@NonNull DataSheetClient.ExporterBuilder dataSheetExporter) {
final String batchId = UUIDBuilder.generateLongUuid(false);
final String scene = dataSheetExporter.scene();
ExportContext context = ExportContext.builder().batchId(batchId).fileName(dataSheetExporter.fileName())
.operator(dataSheetExporter.operator()).ext(dataSheetExporter.ext()).build();
executor.execute(() -> doExport(context, scene, dataSheetExporter));
return batchId;
}
@Override
public String export(String fileName, @NonNull List<DataSheetClient.ExporterBuilder> dataSheetExporters) {
checkMultiExporters(fileName, dataSheetExporters);
final String batchId = UUIDBuilder.generateLongUuid(false);
ExportContext context = ExportContext.builder()
.batchId(batchId)
.fileName(fileName)
.operator(dataSheetExporters.get(0).operator())
.writer(EasyExcel.write(new ByteArrayOutputStream())
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.build())
.progressMap(dataSheetExporters.stream().collect(Collectors.toMap(e -> e.sheetName(), e -> 0)))
.build();
executor.execute(() -> dataSheetExporters.forEach(e -> doExport(context, e.scene(), e)));
return batchId;
}
void doExport(final ExportContext context, final String scene, DataSheetClient.ExporterBuilder dataSheetExporter) {
try {
final File tempFile = File.createTempFile(context.getBatchId(), ".tmp");
tempFile.deleteOnExit();
log.info("DataSheetAsyncExporter export() scene={} context={}, tempFile->{}", scene, context, tempFile.getAbsolutePath());
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) {
DataSheetClient.Exporter exporter = dataSheetExporter.onProgress((progress, errorMsg) -> {
if (context.isMultiSheet()) {
onMultiSheetProgress(progress, errorMsg, context, tempFile, dataSheetExporter.sheetName());
} else {
onProgress(progress, errorMsg, context, tempFile);
}
}).build();
Preconditions.checkState(exporter != null);
exporter.export(out);
} catch (Exception e) {
log.error("DataSheetAsyncExporter export() FAIL scene={} context={} ", scene, context, e);
} finally {
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void checkMultiExporters(String fileName, List<DataSheetClient.ExporterBuilder> dataSheetExporters) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(fileName), "导出文件名不能为空");
Preconditions.checkArgument(!CollectionUtils.isEmpty(dataSheetExporters), "导出参数不能为空");
dataSheetExporters.forEach(e -> Preconditions.checkArgument(e.format() == DataSheetClient.ExportFormat.EXCEL, "导出格式必须为EXCEL"));
dataSheetExporters.forEach(e -> Preconditions.checkArgument(!Strings.isNullOrEmpty(e.sheetName()), "表单名不能为空"));
long sheetCount = dataSheetExporters.stream().map(DataSheetClient.ExporterBuilder::sheetName).distinct().count();
Preconditions.checkArgument(sheetCount == dataSheetExporters.size(), "表单名不能重复");
}
private String buildRedisKey(String batchId) {
return KeyBuilder.build(appRuntime, "data_export", batchId);
}
private void onProgress(Integer progress, String errorMsg, ExportContext context, File tempFile) {
log.info("DataSheetAsyncExporter onProgress() progress={} errorMsg={} context={} tempFile={}", progress, errorMsg, context, tempFile.getAbsolutePath());
String downloadUrl = StringUtils.EMPTY;
if (Strings.isNullOrEmpty(errorMsg) && progress >= 100) {
try {
// 只有导出成功并完成才开始上传文件
byte[] bytes = IoUtils.toByteArray(new BufferedInputStream(new FileInputStream(tempFile)));
downloadUrl = fileUploader.apply(FileUploadParam.builder()
.bytes(bytes).fileName(context.getFileName())
.operator(context.getOperator()).ext(context.getExt()).build()).getDownloadUrl();
log.info("DataSheetAsyncExporter onProgress() DONE context={}, downloadUrl={}", context, downloadUrl);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 将导出的状态放入 redis 后续通过getProgress()来获取
String jsonString = JSONObject.toJSONString(Progress.builder().fileName(context.getFileName()).progress(progress)
.downloadUrl(downloadUrl).errorMsg(errorMsg).build());
redisTemplate.opsForValue().set(buildRedisKey(context.getBatchId()), jsonString, PROGRESS_EXPIRE_DAYS, TimeUnit.DAYS);
}
private void onMultiSheetProgress(Integer progress, String errorMsg, ExportContext context, File tempFile, String sheetName) {
log.info("DataSheetAsyncExporter onMultiSheetProgress() context={}, progress={} errorMsg={} tempFile={} sheetName={}", context, progress, errorMsg, tempFile.getAbsolutePath(), sheetName);
context.getProgressMap().put(sheetName, progress);
Progress totalProgress = getMultiSheetProgress(context, errorMsg);
if (!Strings.isNullOrEmpty(errorMsg) && progress < 0) {
context.getWriter().write(ImmutableList.of(ImmutableList.of(errorMsg)),
EasyExcel.writerSheet(sheetName).build());
} else if (progress >= 100) {
EasyExcel.read(tempFile, new ReadAndWriteListener(context, sheetName)).sheet(sheetName).doRead();
}
if (totalProgress.isDone()) {
// 只有所有表单都导出完成才开始上传文件
context.getWriter().finish();
ByteArrayOutputStream outputStream = (ByteArrayOutputStream) context.getWriter().writeContext()
.writeWorkbookHolder().getOutputStream();
String downloadUrl = fileUploader.apply(FileUploadParam.builder()
.bytes(outputStream.toByteArray()).fileName(context.getFileName())
.operator(context.getOperator()).build()).getDownloadUrl();
log.info("DataSheetAsyncExporter onMultiSheetProgress() DONE context={}, downloadUrl={}", context, downloadUrl);
totalProgress.setDownloadUrl(downloadUrl);
}
// 将导出的状态放入 redis 后续通过getProgress()来获取
String jsonString = JSONObject.toJSONString(totalProgress);
redisTemplate.opsForValue().set(buildRedisKey(context.getBatchId()), jsonString, PROGRESS_EXPIRE_DAYS, TimeUnit.DAYS);
}
private Progress getMultiSheetProgress(ExportContext context, String errorMsg) {
Map<String, Integer> progressMap = context.getProgressMap();
int totalProgress = progressMap.values().stream().mapToInt(a -> a).sum() / progressMap.size();
boolean isDone = progressMap.values().stream().allMatch(a -> a < 0 || a >= 100);
if (isDone && totalProgress != 100) {
// 所有导出都已完成但其中有失败将progress设为-1
totalProgress = -1;
}
return Progress.builder()
.fileName(context.getFileName())
.progress(totalProgress)
.errorMsg(errorMsg)
.downloadUrl(StringUtils.EMPTY)
.build();
}
/**
* 使用在export()返回的 batchId 来查询导出进度
*
* @param batchId
* @return
*/
@Override
public Progress getProgress(String batchId) {
String res = redisTemplate.opsForValue().get(buildRedisKey(batchId));
if (Strings.isNullOrEmpty(res)) {
return Progress.builder().fileName(StringUtils.EMPTY)
.progress(0).errorMsg(StringUtils.EMPTY).downloadUrl(StringUtils.EMPTY).build();
}
return JSONObject.parseObject(res, Progress.class);
}
@Data
public static class Builder {
private RedisTemplate<String, String> redisTemplate;
private AppRuntime appRuntime;
private Function<FileUploadParam, FileUploadResult> fileUploader;
private TaskExecutor executor;
public Builder redisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
return this;
}
public Builder appRuntime(AppRuntime appRuntime) {
this.appRuntime = appRuntime;
return this;
}
public Builder fileUploader(Function<FileUploadParam, FileUploadResult> fileUploader) {
this.fileUploader = fileUploader;
return this;
}
public Builder executor(TaskExecutor executor) {
this.executor = executor;
return this;
}
public DataSheetClient.AsyncExporter build() {
if (appRuntime.getEnv() == AppEnvEnum.unittest) {
return new DataSheetClient.AsyncExporter() {
@Override
public String export(DataSheetClient.@NonNull ExporterBuilder dataSheetExporter) {
return "8888";
}
@Override
public String export(String fileName, @NonNull List<DataSheetClient.ExporterBuilder> dataSheetExporters) {
return "8888";
}
@Override
public Progress getProgress(String batchId) {
return Progress.builder().progress(100).downloadUrl("http://test/test.jpg").build();
}
};
}
return new DataSheetAsyncExporter(redisTemplate, this.appRuntime, this.fileUploader, this.executor);
}
}
private static class ReadAndWriteListener extends AnalysisEventListener<Map<Integer, String>> {
private ExportContext context;
private WriteSheet sheet;
public ReadAndWriteListener(ExportContext context, String sheetName) {
this.context = context;
this.sheet = EasyExcel.writerSheet(sheetName).build();
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
List<String> row = data.entrySet().stream().sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue).collect(Collectors.toList());
this.context.getWriter().write(ImmutableList.of(row), sheet);
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
List<String> row = headMap.entrySet().stream().sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue).collect(Collectors.toList());
this.context.getWriter().write(ImmutableList.of(row), sheet);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
@Data
@lombok.Builder
@NoArgsConstructor
@AllArgsConstructor
static class ExportContext {
private String batchId;
private String fileName;
private String operator;
private ExcelWriter writer;
private Map<String, Integer> progressMap;
private JSONObject ext;
private boolean isMultiSheet() {
return writer != null && !CollectionUtils.isEmpty(progressMap);
}
}
}

View File

@ -0,0 +1,90 @@
package cn.axzo.foundation.excel.support.impl;
import cn.axzo.foundation.excel.support.DataSheetClient;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.beans.BeanMap;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class DataSheetClientImpl implements DataSheetClient {
private final static String UNKNOWN_OPERATOR = "unknown@fiture.com";
@Override
public ExporterBuilder exportBuilder() {
DataSheetExporter exporter = new DataSheetExporter();
exporter.setOnCompleted(this::reportExport);
return exporter;
}
@Override
public ImporterBuilder importBuilder() {
DataSheetImporter importer = new DataSheetImporter();
importer.setOnCompleted(this::reportImport);
return importer;
}
public void reportExport(DataSheetClient.ExportResp resp) {
String operator = resp.getOperator();
ReportReq req = ReportReq.builder()
.scene(resp.getScene())
.action(ReportReq.Action.EXPORT)
.resp(BeanMap.create(resp))
.rowCount(resp.getRowCount())
.elapsedMillis(resp.getElapsedMillis())
.operatorId(operator)
.operatorName(operator)
.build();
report(req);
}
public void reportImport(DataSheetClient.ImportResp resp) {
String operator = resp.getOperator();
JSONObject jsonObject = new JSONObject(new HashMap<>(BeanMap.create(resp)));
ReportReq req = ReportReq.builder()
.scene(resp.getScene())
.action(ReportReq.Action.IMPORT)
.resp(jsonObject.fluentRemove("lines").fluentRemove("meta"))
.rowCount(resp.getRowCount().longValue())
.elapsedMillis(resp.getElapsedMillis())
.operatorId(operator)
.operatorName(operator)
.build();
report(req);
}
private boolean report(ReportReq req) {
return true;
}
@Data
@lombok.Builder
@NoArgsConstructor
@AllArgsConstructor
private static class ReportReq {
String appName;
String scene;
Action action;
Map resp;
Long rowCount;
Long elapsedMillis;
String filePath;
String operatorId;
String operatorName;
String operatorTenantId;
public enum Action {
IMPORT,
EXPORT;
}
}
}

View File

@ -0,0 +1,698 @@
package cn.axzo.foundation.excel.support.impl;
import cn.axzo.foundation.excel.support.DataSheetClient;
import cn.axzo.foundation.exception.BusinessException;
import cn.axzo.foundation.page.PageReq;
import cn.axzo.foundation.page.PageResp;
import cn.axzo.foundation.result.ResultCode;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.StringUtils;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.handler.AbstractCellWriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.style.AbstractCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.alibaba.excel.write.style.column.SimpleColumnWidthStyleStrategy;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.*;
import com.opencsv.CSVWriter;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static cn.axzo.foundation.excel.support.DataSheetClient.CellMeta.*;
import static cn.axzo.foundation.excel.support.DataSheetClient.Meta.*;
@Slf4j
public class DataSheetExporter extends DataSheetClient.ExporterBuilder implements DataSheetClient.Exporter {
/** 完成后处理事件. */
@Setter
private Consumer<DataSheetClient.ExportResp> onCompleted;
@Override
public DataSheetClient.Exporter build() {
if (!CollectionUtils.isEmpty(this.fields())) {
if (this.columnMap() != null) {
throw new RuntimeException("column map is present, please remove column or fields");
}
if (this.meta() != null) {
throw new RuntimeException("meta is present, please remove meta or fields");
}
ImmutableMap<String, BiFunction<JSONObject, Integer, Object>> columns = this.fields().stream()
.collect(ImmutableMap.toImmutableMap(DataSheetClient.ExportField::getHeader, DataSheetClient.ExportField::getReader));
this.columnMap(columns);
ImmutableMap<String, List<String>> multiLineHeadMap = this.fields().stream()
.filter(f -> !CollectionUtils.isEmpty(f.getHeaderLines()))
.collect(ImmutableMap.toImmutableMap(DataSheetClient.ExportField::getHeader, DataSheetClient.ExportField::getHeaderLines));
this.multiLineHeadMap(multiLineHeadMap);
DataSheetClient.Meta meta = DataSheetClient.Meta.builder()
.templateCode(this.scene())
.version(Optional.ofNullable(this.version()).orElse("00"))
.cellMetas(this.fields().stream().map(DataSheetClient.ExportField::toCellMeta).collect(Collectors.toList()))
.build();
this.meta(meta);
}
Preconditions.checkArgument(this.scene() != null, "scene不能为空");
Preconditions.checkArgument(this.format() != null, "format不能为空");
Preconditions.checkArgument(this.rowSupplier() != null, "rowSupplier不能为空");
Preconditions.checkArgument(this.columnMap() != null, "columnMap不能为空");
return this;
}
@Override
public DataSheetClient.ExportResp export(OutputStream outputStream) {
Preconditions.checkArgument(outputStream != null, "outputStream不能为空");
long exportedCount = 0;
Exporter exporter = null;
Stopwatch stopwatch = Stopwatch.createStarted();
String errorMsg = "";
try {
exporter = getDataExporter(outputStream);
long pageNumber = 1;
while (limit() > exportedCount) {
// 每页最多允许数量
long pageMaxSize = Math.min(limit() - exportedCount, pageSize());
if (pageMaxSize <= 0) {
break;
}
PageReq pageParam = PageReq.builder()
.page((int) pageNumber)
// 这里使用pageSize而不是pageMaxSize否则会导致最后一次查询的数据和前面的查询数据重复
.pageSize(pageSize().intValue())
.build();
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, request data, pageParam={}", scene(), pageParam);
}
PageResp<JSONObject> result = rowSupplier().apply(pageParam);
// nextPageMaxSize少于查询的pageSize说明达到了最大导出数只提取允许的数量
if (result.getData().size() > pageMaxSize && pageMaxSize < pageSize()) {
result = PageResp.<JSONObject>builder().current(result.getCurrent())
.size(result.getSize()).total(result.getTotal())
.data(result.getData().subList(0, (int) pageMaxSize))
.build();
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, result data, and split result size to pageMaxSize={}, size={}",
scene(), pageMaxSize, result.getData().size());
}
} else {
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, result data, size={}",
scene(), result.getData().size());
}
}
if (pageNumber == 1) {
if (!CollectionUtils.isEmpty(multiLineHeadMap())) {
List<List<String>> multiLineHeads = columnMap().keySet().stream()
.map(headName -> multiLineHeadMap().getOrDefault(headName, ImmutableList.of(headName)))
.collect(Collectors.toList());
exporter.writeMultiLineHeads(multiLineHeads);
} else {
exporter.writeHead(Lists.newArrayList(columnMap().keySet()));
}
// 第一页数据需要写顶部提示内容及表头
if (topHintsSupplier() != null) {
exporter.writeTopHints(topHintsSupplier().apply(result));
}
}
writeData(exporter, result);
exportedCount += result.getData().size();
if (onProgress() != null) {
//XXX 不知道最大的页数这里模拟下进度保证不超过 100%
int progress = Math.min((int) pageNumber, 90);
onProgress().accept(progress, "");
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, progress={}", scene(), progress);
}
}
if (result.getData().size() < result.getSize()) {
break;
}
pageNumber += 1;
}
} catch (Exception e) {
errorMsg = e.getMessage();
if (!StringUtils.isEmpty(errorMsg) && onProgress() != null) {
onProgress().accept(-1, errorMsg);
log.error("-------ExportClient ERROR [{}]------, progress={}", scene(), -1);
}
throw e;
} finally {
if (exporter != null) {
exporter.finish();
if (StringUtils.isEmpty(errorMsg) && onProgress() != null) {
onProgress().accept(100, "");
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, progress={}", scene(), 100);
}
}
}
}
stopwatch.stop();
DataSheetClient.ExportResp exportResp = DataSheetClient.ExportResp.builder()
.scene(scene())
.exportedCount(exportedCount)
.rowCount(exporter.getRowCount())
.columnCount(exporter.getColumnCount())
.dataRowCount(exporter.getDataRowCount())
.elapsedMillis(stopwatch.elapsed(TimeUnit.MILLISECONDS))
.operator(operator())
.build();
if (onCompleted != null) {
onCompleted.accept(exportResp);
}
log.info("-------ExportClient[{}]------, finish export, exportResp={}", scene(), exportResp);
return exportResp;
}
@Override
public DataSheetClient.ExportResp export(HttpServletResponse response) {
return export(response, null);
}
@Override
public DataSheetClient.ExportResp export(HttpServletResponse response, String filename) {
Preconditions.checkArgument(response != null, "response不能为空");
if (filename == null) {
filename = getFilename(format().getSuffix());
}
try {
filename = URLEncoder.encode(filename, CharEncoding.UTF_8);
} catch (UnsupportedEncodingException e) {
throw new BusinessException(ResultCode.RUNTIME_EXCEPTION.getErrorCode(), "导出失败,请稍后重试");
}
response.setCharacterEncoding(CharEncoding.UTF_8);
response.setContentType(format().getContentType());
try {
response.setHeader("Content-Disposition", "attachment;filename=" + filename);
log.info("-------ExportClient[{}]------, write to servlet response, contentType={}, filename={}",
scene(), format().getContentType(), filename);
return export(response.getOutputStream());
} catch (Exception ex) {
log.error("导出数据异常,文件名:" + filename, ex);
throw new BusinessException(ResultCode.RUNTIME_EXCEPTION.getErrorCode(), "导出失败,请稍后重试");
}
}
private Exporter getDataExporter(OutputStream outputStream) {
return format() == DataSheetClient.ExportFormat.CSV ?
new CsvExporter(outputStream, scene(), debugEnabled()) :
new ExcelExporter(sheetName(), outputStream, scene(), meta(), debugEnabled(), autoTrim());
}
private String getFilename(String suffix) {
if (fileName() != null) {
return fileName();
}
String formatDatetime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
return (Strings.isNullOrEmpty(sheetName()) ? "" : sheetName() + "-" + formatDatetime) + suffix;
}
private void writeData(Exporter exporter, PageResp<JSONObject> page) {
if (CollectionUtils.isEmpty(page.getData())) {
return;
}
List<JSONObject> rowPage = page.getData();
if (rowConverter() != null) {
rowPage = page.getData().stream()
.map(row -> {
List<JSONObject> converted = rowConverter().apply(row);
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, row converted, row={}, converted={}",
scene(), row, converted);
}
return converted;
})
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
if (pageConverter() != null) {
final List<JSONObject> originRowPage = rowPage;
rowPage = pageConverter().apply(rowPage);
if (debugEnabled()) {
log.info("-------ExportClient[{}]------, page converted, old={}, converted={}",
scene(), originRowPage, rowPage);
}
}
int rowIndex = (int) exporter.getDataRowCount();
List<JSONObject> finalRowPage = rowPage;
List<List<Object>> rows = IntStream.range(0, finalRowPage.size())
.mapToObj(index -> columnMap().values().stream()
.map(v -> v.apply(finalRowPage.get(index), rowIndex + index))
.collect(Collectors.toList()))
.collect(Collectors.toList());
rows.forEach(exporter::writeRow);
}
private class CsvExporter extends RowColumnCounter implements Exporter {
private CSVWriter writer;
private String scene;
private boolean debugEnabled;
private CsvExporter(OutputStream outputStream, String scene, boolean debugEnabled) {
try {
// encoding
outputStream.write(0xef);
outputStream.write(0xbb);
outputStream.write(0xbf);
} catch (IOException e) {
throw new BusinessException(ResultCode.RUNTIME_EXCEPTION.getErrorCode(), "导出数据失败");
}
writer = new CSVWriter(new OutputStreamWriter(outputStream));
this.scene = scene;
this.debugEnabled = debugEnabled;
}
@Override
public void writeTopHints(List<String> topHints) {
if (CollectionUtils.isEmpty(topHints)) {
return;
}
topHints.forEach(t -> writer.writeNext(new String[]{t}));
countTopHints(topHints.size());
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write top={}", scene, topHints);
}
}
@Override
public void writeHead(List<String> head) {
writer.writeNext(head.toArray(new String[0]));
countHead(head.size());
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write head={}", scene, head);
}
}
@Override
public void writeMultiLineHeads(List<List<String>> multiLineHeads) {
int lineNum = multiLineHeads.stream().mapToInt(List::size).max().orElse(0);
IntStream.range(0, lineNum).forEach(i -> {
List<String> heads = multiLineHeads.stream().map(head -> {
// 当表头的行数小于最大行数时最上面的行中表头内容写入空字符串
int line = i - lineNum + head.size();
return line < 0 ? StringUtils.EMPTY : head.get(line);
}).collect(Collectors.toList());
writeHead(heads);
});
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write multi line heads={}", scene, multiLineHeads);
}
}
@Override
public void writeRow(List<Object> row) {
String[] arrayContents = row.stream()
.map(c -> c == null ? StringUtils.EMPTY : c.toString())
.toArray(String[]::new);
writer.writeNext(arrayContents);
countRow(row.size());
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write row={}", scene, row);
}
}
@Override
public void finish() {
try {
writer.close();
} catch (IOException e) {
throw new BusinessException(ResultCode.RUNTIME_EXCEPTION.getErrorCode(), "导出数据失败");
}
}
}
private class ExcelExporter extends RowColumnCounter implements Exporter {
private final Integer DEFAULT_WIDTH = 6;
private ExcelWriter writer;
private WriteSheet sheet;
private String scene;
private boolean debugEnabled;
private HeaderCommentWriteHandler commentWriteHandler;
private DataValidationWriteHandler validationWriteHandler;
private int topHintsRowCount = 0;
private ExcelExporter(String sheetName, OutputStream outputStream, String scene, DataSheetClient.Meta meta,
boolean debugEnabled, Boolean autoTrim) {
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(outputStream);
if (Objects.nonNull(meta) && !CollectionUtils.isEmpty(meta.getCellMetas())) {
Integer width = meta.getCellMetas().get(0).getWidth();
excelWriterBuilder.registerWriteHandler(new SimpleColumnWidthStyleStrategy(Objects.nonNull(width) ?
width : DEFAULT_WIDTH));
} else {
excelWriterBuilder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
}
if (meta != null) {
excelWriterBuilder.registerWriteHandler(new AbstractCellStyleStrategy() {
private CellStyle wrapStyle;
@Override
protected void setHeadCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
}
@Override
protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
if (cell.getColumnIndex() >= meta.getCellMetas().size()) {
return;
}
DataSheetClient.CellMeta cellMeta = meta.getCellMetas().get(cell.getColumnIndex());
if (cellMeta != null && BooleanUtils.isTrue(cellMeta.getWrapText())) {
cell.setCellStyle(wrapStyle);
}
}
});
commentWriteHandler = new HeaderCommentWriteHandler(meta);
validationWriteHandler = new DataValidationWriteHandler(meta);
excelWriterBuilder.registerWriteHandler(new AbstractCellWriteHandler() {
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean isHeadRow = cell.getRowIndex() == topHintsRowCount;
commentWriteHandler.afterCellDispose(cell, isHeadRow);
validationWriteHandler.afterCellDispose(cell, isHeadRow);
}
});
}
writer = excelWriterBuilder.autoTrim(autoTrim).build();
sheet = EasyExcel.writerSheet(sheetName).build();
this.scene = scene;
this.debugEnabled = debugEnabled;
}
@Override
public void writeTopHints(List<String> topHints) {
if (CollectionUtils.isEmpty(topHints)) {
return;
}
countTopHints(topHints.size());
topHintsRowCount += topHints.size();
writer.write(topHints.stream().map(ImmutableList::of).collect(Collectors.toList()), sheet);
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write top={}", scene, topHints);
}
}
@Override
public void writeHead(List<String> head) {
sheet.setHead(head.stream().map(Lists::newArrayList).collect(Collectors.toList()));
countHead(head.size());
// XXX 写入head后需要调用一次write()确保excel中有数据否则数据为空时将出现内容异常
writer.write(ImmutableList.of(), sheet);
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write head={}", scene, head);
}
}
@Override
public void writeMultiLineHeads(List<List<String>> multiLineHeads) {
// XXX setHead()方法有副作用内部逻辑会改变传入的list, 因此需要确保传入的list可操作
sheet.setHead(multiLineHeads.stream().map(Lists::newArrayList).collect(Collectors.toList()));
int lineNum = multiLineHeads.stream().mapToInt(List::size).max().orElse(0);
IntStream.range(0, lineNum).forEach(i -> countHead(multiLineHeads.size()));
// XXX 写入head后需要调用一次write()确保excel中有数据否则数据为空时将出现内容异常
writer.write(ImmutableList.of(), sheet);
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write multi line heads={}", scene, multiLineHeads);
}
}
@Override
public void writeRow(List<Object> row) {
writer.write(ImmutableList.of(row), sheet);
countRow(row.size());
if (debugEnabled) {
log.info("-------ExportClient[{}]------, write row={}", scene, row);
}
}
@Override
public void finish() {
writer.finish();
}
}
/**
* Excel添加头部的批注
* XXX: 此处不能继承AbstractCellWriteHandler 会导致 mybatis typehandler 扫描时被加载
* 导致老项目没有使用 easyexcel2.2 的项目启动失败
*/
private static class HeaderCommentWriteHandler {
private static final Map<List<String>, String> rangeBoundTypes = ImmutableMap.of(
ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_OPEN), RANGE_TYPE_OPEN_OPEN,
ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_CLOSED), RANGE_TYPE_OPEN_CLOSED,
ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_OPEN), RANGE_TYPE_CLOSED_OPEN,
ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_CLOSED), RANGE_TYPE_CLOSED_CLOSED);
private String templateCode;
private String version;
private Map<String, DataSheetClient.CellMeta> cellMetaMap;
private Set<DataSheetClient.CellMeta.Type> typeWithParams = ImmutableSet.of(
DataSheetClient.CellMeta.Type.RANGE,
DataSheetClient.CellMeta.Type.DATE,
DataSheetClient.CellMeta.Type.DATETIME
);
private HeaderCommentWriteHandler(DataSheetClient.Meta meta) {
this.templateCode = meta.getTemplateCode();
this.version = meta.getVersion();
this.cellMetaMap = Maps.uniqueIndex(meta.getCellMetas(), DataSheetClient.CellMeta::getName);
}
public void afterCellDispose(Cell cell, boolean isHead) {
// 只添加表头行的批注
if (!isHead) {
return;
}
List<String> comments = Lists.newArrayList();
if (cell.getColumnIndex() == 0) {
String content = Joiner.on("_").join(ImmutableList.of(templateCode, version));
comments.add(buildComment(PATTERN_VERSION_OPEN, content));
}
DataSheetClient.CellMeta cellMeta = cellMetaMap.get(cell.getStringCellValue());
if (cellMeta == null) {
return;
}
// 格式 key_type_mandatory
String content = Joiner.on("_").join(ImmutableList.of(
cellMeta.getKey(),
cellMeta.getType().getCode(),
Boolean.TRUE.equals(cellMeta.getMandatory()) ? "1" : "0"));
if (typeWithParams.contains(cellMeta.getType())) {
content = content.concat("_").concat(getParamsByType(cellMeta));
}
comments.add(buildComment(PATTERN_CELL_OPEN, content));
// 一个Cell只能写入一次Comment
writeComment(cell, Joiner.on("\n").join(comments));
}
private String getParamsByType(DataSheetClient.CellMeta cellMeta) {
switch (cellMeta.getType()) {
case RANGE:
String lowerType = cellMeta.getParams().getString(EXT_KEY_RANGE_LOWER_TYPE);
String upperType = cellMeta.getParams().getString(EXT_KEY_RANGE_UPPER_TYPE);
return rangeBoundTypes.get(ImmutableList.of(lowerType, upperType));
case DATE:
case DATETIME:
return cellMeta.getParams().getString(EXT_KEY_DATETIME_FORMAT);
default:
return StringUtils.EMPTY;
}
}
private void writeComment(Cell cell, String comment) {
int columnIndex = cell.getColumnIndex();
int rowIndex = cell.getRowIndex();
Drawing<?> drawing = cell.getSheet().createDrawingPatriarch();
Comment cellComment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0,
// 控制批注的位置
columnIndex, rowIndex, columnIndex + 2, rowIndex + 2));
cellComment.setString(new XSSFRichTextString(comment));
cell.setCellComment(cellComment);
}
private String buildComment(String patternOpen, String content) {
return patternOpen.concat(content).concat(PATTERN_CLOSE);
}
}
/**
* Excel添加校验数据
* XXX: 此处不能继承AbstractCellWriteHandler 会导致 mybatis typehandler 扫描时被加载
* 导致老项目没有使用 easyexcel2.2 的项目启动失败
*/
private static class DataValidationWriteHandler {
private Map<String, DataSheetClient.CellMeta> cellMetaMap;
private DataValidationWriteHandler(DataSheetClient.Meta meta) {
this.cellMetaMap = Maps.uniqueIndex(meta.getCellMetas(), DataSheetClient.CellMeta::getName);
}
public void afterCellDispose(Cell cell, boolean isHead) {
// 只在写入表头数据时才写入校验数据
if (!isHead) {
return;
}
DataSheetClient.CellMeta cellMeta = cellMetaMap.get(cell.getStringCellValue());
// 目前仅支持将可选值作为下拉选项的校验数据
if (cellMeta == null || CollectionUtils.isEmpty(cellMeta.getOptions())) {
return;
}
String[] options = cellMeta.getOptions().stream().map(String::valueOf).toArray(String[]::new);
Sheet currentSheet = cell.getSheet();
Workbook workbook = currentSheet.getWorkbook();
// 直接将校验数据作为列表项的方式有数据大小限制(不超过256个字符), 因此需要将校验数据放在一个新增的隐藏表单中
// 以表头的名字来命名新增的表单
String sheetName = cell.getStringCellValue();
Sheet validationSheet = workbook.createSheet(sheetName);
workbook.setSheetHidden(workbook.getSheetIndex(validationSheet), true);
// 将可选项放在新增的隐藏表单里
for (int i = 0; i < options.length; i++) {
Row rowInValidationSheet = validationSheet.createRow(i);
Cell cellInValidationSheet = rowInValidationSheet.createCell(0);
cellInValidationSheet.setCellValue(options[i]);
}
// 设置列表校验数据的公式 [表名!开始列开始行:结束列结束行]
String listFormula = sheetName + "!$A$1:$A$" + options.length;
DataValidationHelper dataValidationHelper = currentSheet.getDataValidationHelper();
DataValidationConstraint constraint = dataValidationHelper.createFormulaListConstraint(listFormula);
// 从下一行开始10000行数据都增加校验数据
CellRangeAddressList addressList = new CellRangeAddressList(cell.getRowIndex() + 1,
cell.getRowIndex() + 10001, cell.getColumnIndex(), cell.getColumnIndex());
DataValidation dataValidation = dataValidationHelper.createValidation(constraint, addressList);
dataValidation.setSuppressDropDownArrow(true);
dataValidation.setShowErrorBox(true);
currentSheet.addValidationData(dataValidation);
}
}
private class RowColumnCounter {
@Getter
private long rowCount = 0;
@Getter
private long columnCount = 0;
@Getter
private long dataRowCount = 0;
protected void countTopHints(int topHintsRowCount) {
rowCount += topHintsRowCount;
columnCount = Math.max(columnCount, 1);
}
protected void countHead(int headColumnCount) {
rowCount += 1;
columnCount = Math.max(columnCount, headColumnCount);
}
protected void countRow(int columnCount) {
rowCount += 1;
this.columnCount = Math.max(this.columnCount, columnCount);
dataRowCount += 1;
}
}
private interface Exporter {
void writeTopHints(List<String> topHints);
void writeHead(List<String> head);
void writeMultiLineHeads(List<List<String>> multiLineHeads);
void writeRow(List<Object> row);
void finish();
long getRowCount();
long getColumnCount();
long getDataRowCount();
}
}

View File

@ -0,0 +1,595 @@
package cn.axzo.foundation.excel.support.impl;
import cn.axzo.foundation.excel.support.DataSheetClient;
import cn.axzo.foundation.exception.BusinessException;
import cn.axzo.foundation.util.Regex;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.*;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static cn.axzo.foundation.excel.support.DataSheetClient.CellMeta.*;
import static cn.axzo.foundation.excel.support.DataSheetClient.Meta.*;
import static cn.axzo.foundation.excel.support.DataSheetClient.ResultCode.IMPORT_CELL_CONVERT_FAILED;
/**
* 使用ali的EasyExcel来读入excel/csv文件
*
* @author yuanyi
*/
@Slf4j
public class DataSheetImporter extends DataSheetClient.ImporterBuilder {
private static final String LINE_KEY_ERRORS = "errors";
private static final String LINE_KEY_ROW_INDEX = "rowIndex";
/**
* 完成后处理事件.
*/
@Setter
private Consumer<DataSheetClient.ImportResp> onCompleted;
@Override
public DataSheetClient.Importer build() {
Preconditions.checkArgument(this.scene() != null, "scene不能为空");
Preconditions.checkArgument(this.format() != null, "format不能为空");
// TODO: 当支持更多format的时候需要生成对应的Importer实例
return ExcelImporter.builder()
.scene(scene())
.tableName(tableName())
.meta(meta())
.ignoreHeaderMeta(ignoreHeaderMeta())
.debugEnabled(debugEnabled())
.allowMaxLineCount(allowMaxLineCount())
.headerConverter(headerConverter())
.includeLineErrors(includeLineErrors())
.ignoreUnknownColumn(ignoreUnknownColumn())
.autoTrim(autoTrim())
.onCompleted(onCompleted)
.operator(operator())
.build();
}
@Builder
private static class ExcelImporter implements DataSheetClient.Importer {
private static final Map<String, List<String>> rangeBoundTypes = ImmutableMap.of(
RANGE_TYPE_OPEN_OPEN, ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_OPEN),
RANGE_TYPE_OPEN_CLOSED, ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_CLOSED),
RANGE_TYPE_CLOSED_OPEN, ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_OPEN),
RANGE_TYPE_CLOSED_CLOSED, ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_CLOSED));
private String scene;
private String tableName;
private DataSheetClient.Meta meta;
private Boolean ignoreHeaderMeta;
private boolean debugEnabled;
private Integer allowMaxLineCount;
private boolean includeLineErrors;
private boolean ignoreUnknownColumn;
private Boolean autoTrim;
private Function<String, String> headerConverter;
private Consumer<DataSheetClient.ImportResp> onCompleted;
private String operator;
@Override
public DataSheetClient.ImportResp<JSONObject> readAll(InputStream inputStream) {
Stopwatch stopwatch = Stopwatch.createStarted();
NoModelDataListener dataListener = new NoModelDataListener(headerConverter, autoTrim);
EasyExcel.read(inputStream, dataListener)
.autoTrim(autoTrim)
.extraRead(CellExtraTypeEnum.COMMENT).sheet(tableName).doRead();
if (allowMaxLineCount != null && dataListener.lines.size() > allowMaxLineCount) {
throw DataSheetClient.ResultCode.IMPORT_LINES_REACHED_LIMIT
.toException("导入的数据超过了最大行数" + allowMaxLineCount);
}
if (BooleanUtils.isNotTrue(this.ignoreHeaderMeta)) {
// 聚合来自入参和文件批注的meta信息如果都存在使用入参的meta覆盖文件批注的meta
this.meta = mergeMeta(this.meta, parseMetaFromData(dataListener));
}
filterHeadMap(dataListener);
filterLines(dataListener);
validateHeaders(dataListener);
validateMeta(dataListener);
List<String> headers = parseHeaders(dataListener);
List<JSONObject> lines = parseLines(dataListener);
if (!includeLineErrors) {
// 没有设置includeLineErrors时抛第一个发现的异常
Optional<JSONObject> errorLine = lines.stream()
.filter(line -> line.containsKey(LINE_KEY_ERRORS))
.findFirst();
if (errorLine.isPresent()) {
JSONObject error = errorLine.get().getJSONArray(LINE_KEY_ERRORS).getJSONObject(0);
Integer rowIndex = lines.indexOf(errorLine.get());
Integer columnIndex = error.getInteger("columnIndex");
String errMsg = String.format("第%d行, 第%d列字段[%s]%s", rowIndex + 1, columnIndex + 1,
error.getString("columnName"), error.getString("errorMsg"));
throw new DataSheetClient.DataSheetException(error.getString("subErrorCode"), errMsg, rowIndex, columnIndex);
}
}
DataSheetClient.ImportResp resp = DataSheetClient.ImportResp.<JSONObject>builder()
.scene(scene)
.templateCode(Optional.ofNullable(meta).map(DataSheetClient.Meta::getTemplateCode).orElse(null))
.version(Optional.ofNullable(meta).map(DataSheetClient.Meta::getVersion).orElse(null))
.headers(headers)
.lines(lines)
.headerRowCount(1)
.rowCount(lines.size())
.columnCount(headers.size())
.meta(meta)
.elapsedMillis(stopwatch.elapsed(TimeUnit.MILLISECONDS))
.operator(operator)
.build();
if (null != onCompleted) {
onCompleted.accept(resp);
}
return resp;
}
private void filterHeadMap(NoModelDataListener dataListener) {
if (meta == null) {
return;
}
Map<Integer, String> headMap = dataListener.getHeadMap();
Set<String> ignoreColumnNames = meta.getIgnoreColumnNames();
// 收集需要过滤的列号
Set<Integer> ignoreColumnIndexes = headMap.entrySet().stream()
.filter(entry -> ignoreColumnNames.contains(entry.getValue()))
.map(Map.Entry::getKey).collect(Collectors.toSet());
// 加上指定过滤的列号
ignoreColumnIndexes.addAll(meta.getIgnoreColumnIndexes());
ignoreColumnIndexes.forEach(headMap::remove);
}
private void filterLines(NoModelDataListener dataListener) {
if (meta == null) {
return;
}
Set<Integer> toRemoveLines = ImmutableSet.copyOf(meta.getIgnoreRowIndexes());
List<Map<Integer, String>> lines = dataListener.getLines();
dataListener.setLines(IntStream.range(0, lines.size())
// 这里要加1是因为removeIndex是整个文档的行数来计算包括了header
// 但是lines的数据已经排除了header
.filter(index -> !toRemoveLines.contains(index + 1))
.mapToObj(lines::get)
.collect(Collectors.toList()));
}
private DataSheetClient.Meta parseMetaFromData(NoModelDataListener dataListener) {
Map<Integer, String> headMap = dataListener.getHeadMap();
List<CellExtra> headComments = dataListener.getCellComments().stream()
// 获取第一行Head的批注信息
.filter(cellExtra -> cellExtra.getRowIndex() == 0)
// 过滤带有Cell参数配置的批注信息
.filter(cellExtra -> !Strings.isNullOrEmpty(StringUtils.substringBetween(cellExtra.getText(),
PATTERN_CELL_OPEN, PATTERN_CLOSE)))
// 排序方便找到第一列获取templateCode与version
.sorted(Comparator.comparing(CellExtra::getColumnIndex))
.collect(Collectors.toList());
// 没有批注信息直接返回null会以String来解析值
if (headComments.isEmpty()) {
return null;
}
// 从文件中解析meta信息
JSONObject codeAndVersion = parseTemplateCodeAndVersion(headComments);
List<DataSheetClient.CellMeta> cellMetas = headComments.stream()
.map(cellExtra -> parseCellMeta(cellExtra.getText(), headMap.get(cellExtra.getColumnIndex())))
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (cellMetas.isEmpty()) {
return null;
}
return DataSheetClient.Meta.builder()
.templateCode(codeAndVersion.getString("templateCode"))
.version(codeAndVersion.getString("version"))
.cellMetas(cellMetas)
.ignoreColumnIndexes(parseIgnoreColumns(dataListener))
.ignoreRowIndexes(parseIgnoreRows(dataListener))
.build();
}
private DataSheetClient.Meta mergeMeta(DataSheetClient.Meta metaFromParam, DataSheetClient.Meta metaFromHeader) {
if (metaFromHeader == null || metaFromParam == null) {
return metaFromHeader == null ? metaFromParam : metaFromHeader;
}
// 在两者都存在的时候以文件的meta为准仅仅将入参的CellMetas覆盖文件的CellMetas
if (metaFromHeader.getCellMetas() == null) {
metaFromHeader.setCellMetas(metaFromParam.getCellMetas());
return metaFromHeader;
}
// 使用入参中定义的列信息覆盖文件的cellMetas
Map<String, DataSheetClient.CellMeta> cellMetaMap =
Maps.uniqueIndex(metaFromParam.getCellMetas(), DataSheetClient.CellMeta::getKey);
List<DataSheetClient.CellMeta> cellMetas = metaFromHeader.getCellMetas().stream()
.map(cellMeta -> cellMetaMap.getOrDefault(cellMeta.getKey(), cellMeta))
.collect(Collectors.toList());
metaFromHeader.setCellMetas(cellMetas);
return metaFromHeader;
}
private List<Integer> parseIgnoreRows(NoModelDataListener dataListener) {
return dataListener.getCellComments().stream()
.map(cellExtra -> {
String ignoreKey = StringUtils.substringBetween(cellExtra.getText(),
PATTERN_IGNORE_OPEN, PATTERN_CLOSE);
if (IGNORE_ROW_KEY.equals(ignoreKey) || IGNORE_ROW_AND_COLUMN_KEY.equals(ignoreKey)) {
return cellExtra.getRowIndex();
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private List<Integer> parseIgnoreColumns(NoModelDataListener dataListener) {
return dataListener.getCellComments().stream()
.map(cellExtra -> {
String ignoreKey = StringUtils.substringBetween(cellExtra.getText(),
PATTERN_IGNORE_OPEN, PATTERN_CLOSE);
if (IGNORE_COLUMN_KEY.equals(ignoreKey) || IGNORE_ROW_AND_COLUMN_KEY.equals(ignoreKey)) {
return cellExtra.getColumnIndex();
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public JSONObject parseTemplateCodeAndVersion(List<CellExtra> headComments) {
// 从整个头部的批注获取模版code和版本信息
Optional<String> parsedText = headComments.stream()
.map(comment -> StringUtils.substringBetween(comment.getText(), PATTERN_VERSION_OPEN, PATTERN_CLOSE))
.filter(str -> !Strings.isNullOrEmpty(str))
.findFirst();
if (!parsedText.isPresent()) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_MISSING_VERSION.toException();
}
// 格式为"CODE_VERSION"
List<String> values = Splitter.on("_").splitToList(parsedText.get());
if (values.size() != 2) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_VERSION_FORMAT_ERROR.toException();
}
return new JSONObject()
.fluentPut("templateCode", values.get(0))
.fluentPut("version", values.get(1));
}
private DataSheetClient.CellMeta parseCellMeta(String text, String name) {
String value = StringUtils.substringBetween(text, PATTERN_CELL_OPEN, PATTERN_CLOSE);
if (Strings.isNullOrEmpty(value)) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_MISSING_TYPE
.toException("没有找到列[{}]的类型信息", name);
}
List<String> values = Splitter.on("_").splitToList(value);
// 格式 key_type_mandatory_params...
if (values.size() < 3) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_FORMAT_ERROR
.toException("列[{}]类型的批注格式不对[{}]", name, text);
}
DataSheetClient.CellMeta.Type type = DataSheetClient.CellMeta.Type.from(values.get(1));
JSONObject params = new JSONObject();
if (type == DataSheetClient.CellMeta.Type.RANGE) {
if (values.size() != 4 || Strings.isNullOrEmpty(values.get(3))) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_RANGE_FORMAT_ERROR
.toException("列[{}]范围类型的批注格式不对[{}]", name, text);
}
List<String> boundType = rangeBoundTypes.get(values.get(3));
if (boundType == null) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR
.toException(String.format("列[{}]范围类型的值不对", name));
}
params.put(EXT_KEY_RANGE_LOWER_TYPE, boundType.get(0));
params.put(EXT_KEY_RANGE_UPPER_TYPE, boundType.get(1));
}
JSONObject ext = new JSONObject();
value = StringUtils.substringBetween(text, PATTERN_EXTRA_OPEN, PATTERN_CLOSE);
if (!Strings.isNullOrEmpty(value)) {
ext.putAll(Splitter.on("&").withKeyValueSeparator("=").split(value));
}
return DataSheetClient.CellMeta.builder()
.key(values.get(0))
.name(name)
.type(type)
.mandatory("1".equals(values.get(2)))
.params(params)
.ext(ext)
.build();
}
private List<String> parseHeaders(NoModelDataListener dataListener) {
return dataListener.getHeadMap().keySet().stream()
.sorted()
.map(key -> Strings.nullToEmpty(dataListener.getHeadMap().get(key)))
.collect(Collectors.toList());
}
private List<JSONObject> parseLines(NoModelDataListener dataListener) {
// 如果没有找到meta信息按照key=header(很可能是中文), 类型就为String
if (this.meta == null) {
Map<Integer, String> headerMap = dataListener.getHeadMap();
return dataListener.getLines().stream()
.map(line -> new JSONObject().fluentPutAll(line.entrySet().stream()
.map(entry -> Pair.of(headerMap.get(entry.getKey()), Strings.nullToEmpty(entry.getValue())))
.collect(Collectors.toMap(Pair::getKey, Pair::getValue))))
.collect(Collectors.toList());
}
// 根据meta来校验cell的类型
Map<Integer, String> headerMap = dataListener.getHeadMap();
Map<String, DataSheetClient.CellMeta> cellMetaMap = Maps.uniqueIndex(meta.getCellMetas(),
DataSheetClient.CellMeta::getName);
List<Map<Integer, String>> lines = dataListener.getLines();
return IntStream.range(0, lines.size())
.mapToObj(lineIndex -> {
Map<Integer, String> line = lines.get(lineIndex);
// 收集每一行每一列的转换结果
List<ColumnConvertResp> convertRespList = headerMap.entrySet().stream()
.filter(e -> {
//XXX 当支持忽略列的时候, 只处理定义了cellMeta的数据
if (ignoreUnknownColumn) {
return cellMetaMap.containsKey(e.getValue());
}
return true;
})
.map(entry -> {
Integer columnIndex = entry.getKey();
String header = entry.getValue();
DataSheetClient.CellMeta cellMeta = cellMetaMap.get(header);
return convertType(cellMeta, line.get(columnIndex), lineIndex, columnIndex);
})
.collect(Collectors.toList());
JSONObject row = new JSONObject()
.fluentPutAll(convertRespList.stream()
.filter(ColumnConvertResp::getSuccess)
// convertValue可能为null, 有非必需的字段
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getConvertedValue()), HashMap::putAll));
convertRespList.stream()
.filter(r -> BooleanUtils.isTrue(r.getSuccess()))
.filter(r -> r.getCellMeta().getCellValidator() != null)
.forEach(r -> {
try {
r.getCellMeta().getCellValidator().accept(r.getConvertedValue(), row);
} catch (BusinessException e) {
log.warn("failed to validate cell, resp={}", r, e);
String subErrorCode = null;
if (e instanceof DataSheetClient.DataSheetException) {
subErrorCode = ((DataSheetClient.DataSheetException) e).getSubErrorCode();
}
r.setSuccess(false);
r.setErrorCode(e.getErrorCode());
r.setErrorMsg(e.getErrorMsg());
r.setSubErrorCode(subErrorCode);
}
});
Map<Boolean, List<ColumnConvertResp>> convertRespMap = convertRespList.stream()
.collect(Collectors.groupingBy(ColumnConvertResp::getSuccess));
JSONObject convertedLine = new JSONObject()
.fluentPutAll(convertRespMap.getOrDefault(Boolean.TRUE, ImmutableList.of()).stream()
// convertValue可能为null, 有非必需的字段
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getConvertedValue()), HashMap::putAll));
if (convertRespMap.get(Boolean.FALSE) != null) {
// 转换失败的将失败信息放到errors字段中
convertedLine.put(LINE_KEY_ERRORS, convertRespMap.get(Boolean.FALSE).stream()
.map(ColumnConvertResp::getError)
.collect(Collectors.toList()));
}
convertedLine.put(LINE_KEY_ROW_INDEX, lineIndex);
return convertedLine;
})
.collect(Collectors.toList());
}
private ColumnConvertResp convertType(DataSheetClient.CellMeta cellMeta, String rawValue, int rowIndex, int columnIndex) {
try {
return ColumnConvertResp.builder()
.success(true).cellMeta(cellMeta)
.rawValue(rawValue).convertedValue(cellMeta.convertType(rawValue))
.build();
} catch (BusinessException e) {
log.error("failed to convert type, cellMeta={}, rawValue={}, rowIndex={}, columnIndex={}",
cellMeta, rawValue, rowIndex, columnIndex, e);
String subErrorCode = null;
if (e instanceof DataSheetClient.DataSheetException) {
subErrorCode = ((DataSheetClient.DataSheetException) e).getSubErrorCode();
}
return ColumnConvertResp.builder()
.success(false).cellMeta(cellMeta).rawValue(rawValue)
.columnIndex(columnIndex)
.errorCode(e.getErrorCode())
.errorMsg(e.getErrorMsg())
.subErrorCode(subErrorCode)
.build();
} catch (Exception e) {
log.error("failed to convert type, cellMeta={}, rawValue={}, rowIndex={}, columnIndex={}",
cellMeta, rawValue, rowIndex, columnIndex, e);
String errMsg = String.format("第%d行, 第%d列字段[%s]%s", rowIndex + 1, columnIndex + 1,
cellMeta.getName(), Optional.ofNullable(e.getMessage())
.orElse(IMPORT_CELL_CONVERT_FAILED.getMessage()));
throw new DataSheetClient.DataSheetException(IMPORT_CELL_CONVERT_FAILED.getSubBizCode(), errMsg, rowIndex, columnIndex);
}
}
private void validateHeaders(NoModelDataListener dataListener) {
Map<Integer, String> headMap = dataListener.getHeadMap();
Set<String> headerNames = ImmutableSet.copyOf(headMap.values());
if (headerNames.size() != headMap.size()) {
List<String> columnNames = headMap.values().stream()
.collect(Collectors.groupingBy(Function.identity()))
.entrySet().stream()
.filter(grouped -> grouped.getValue().size() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
throw DataSheetClient.ResultCode.IMPORT_COLUMN_DUPLICATED_NAME.toException(columnNames);
}
}
private void validateMeta(NoModelDataListener dataListener) {
if (this.meta == null) {
return;
}
if (meta.getCellMetas().size() != dataListener.getHeadMap().size()) {
if (dataListener.getHeadMap().size() > meta.getCellMetas().size()) {
if (!ignoreUnknownColumn) {
throw DataSheetClient.ResultCode.IMPORT_COLUMN_MISSING_CELL_META.toException();
}
}
long mandatoryColumnSize = meta.getCellMetas().stream()
.filter(cm -> BooleanUtils.isTrue(cm.getMandatory()))
.count();
if (dataListener.getHeadMap().size() < mandatoryColumnSize) {
throw DataSheetClient.ResultCode.IMPORT_COLUMN_MISSING_CELL_META.toException();
}
}
Set<String> headerNames = ImmutableSet.copyOf(dataListener.getHeadMap().values());
Set<String> cellNames = meta.getCellMetas().stream()
.map(DataSheetClient.CellMeta::getName)
.collect(Collectors.toSet());
if (!ignoreUnknownColumn && !headerNames.equals(cellNames)) {
Set<String> missingNames = Sets.difference(cellNames, headerNames);
Set<String> redundantNames = Sets.difference(headerNames, cellNames);
List<String> columnNames = Stream.of(missingNames, redundantNames)
.flatMap(Set::stream)
.collect(Collectors.toList());
throw DataSheetClient.ResultCode.IMPORT_COLUMN_NAME_NOT_MATCHED.toException(columnNames);
}
meta.getCellMetas().forEach(DataSheetClient.CellMeta::validate);
}
@Slf4j
@Data
private static class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
private Map<Integer, String> headMap = Maps.newHashMap();
private List<Map<Integer, String>> lines = Lists.newArrayList();
private List<CellExtra> cellComments = Lists.newArrayList();
private Function<String, String> headerConverter;
private Boolean autoTrim;
public NoModelDataListener(Function<String, String> headerConverter, Boolean autoTrim) {
this.headerConverter = headerConverter;
this.autoTrim = autoTrim;
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
lines.add(strip(data));
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
Map<Integer, String> convertedMap = headMap;
if (headerConverter != null) {
convertedMap = headMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> headerConverter.apply(e.getValue())));
}
this.headMap.putAll(strip(convertedMap));
}
private Map<Integer, String> strip(Map<Integer, String> data) {
if (BooleanUtils.isFalse(autoTrim)) {
return data;
}
return data.entrySet().stream()
.map(entry -> Maps.immutableEntry(entry.getKey(), StringUtils.strip(entry.getValue(), Regex.WHITESPACE_CHARS)))
// value有可能为null不能直接用Collectors.toMap
.collect(Maps::newHashMap, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
}
/**
* extra是在整个文件被解析后才会调用
*
* @param extra
* @param context
*/
@Override
public void extra(CellExtra extra, AnalysisContext context) {
if (extra.getType() == CellExtraTypeEnum.COMMENT) {
cellComments.add(extra);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class ColumnConvertResp {
private Boolean success;
private DataSheetClient.CellMeta cellMeta;
private String rawValue;
private Object convertedValue;
private Integer columnIndex;
private String errorCode;
private String errorMsg;
private String subErrorCode;
public String getKey() {
return cellMeta.getKey();
}
public JSONObject getError() {
return new JSONObject()
.fluentPut("columnIndex", columnIndex)
.fluentPut("columnKey", cellMeta.getKey())
.fluentPut("columnName", cellMeta.getName())
.fluentPut("rawValue", rawValue)
.fluentPut("errorCode", errorCode)
.fluentPut("errorMsg", errorMsg)
.fluentPut("subErrorCode", subErrorCode);
}
}
}
}

View File

@ -33,6 +33,7 @@
<module>gateway-support-lib</module> <module>gateway-support-lib</module>
<module>event-support-lib</module> <module>event-support-lib</module>
<module>redis-support-lib</module> <module>redis-support-lib</module>
<module>excel-support-lib</module>
</modules> </modules>
<dependencies> <dependencies>