org.ttzero.excel.entity.ListSheet Maven / Gradle / Ivy
/*
* Copyright (c) 2017-2018, [email protected] All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ttzero.excel.entity;
import org.ttzero.excel.annotation.ExcelColumn;
import org.ttzero.excel.annotation.ExcelColumns;
import org.ttzero.excel.annotation.FreezePanes;
import org.ttzero.excel.annotation.HeaderComment;
import org.ttzero.excel.annotation.HeaderStyle;
import org.ttzero.excel.annotation.Hyperlink;
import org.ttzero.excel.annotation.IgnoreExport;
import org.ttzero.excel.annotation.MediaColumn;
import org.ttzero.excel.annotation.StyleDesign;
import org.ttzero.excel.drawing.PresetPictureEffect;
import org.ttzero.excel.manager.Const;
import org.ttzero.excel.processor.ConversionProcessor;
import org.ttzero.excel.processor.Converter;
import org.ttzero.excel.processor.StyleProcessor;
import org.ttzero.excel.reader.Cell;
import org.ttzero.excel.util.StringUtil;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import static org.ttzero.excel.util.ReflectUtil.listDeclaredFieldsUntilJavaPackage;
import static org.ttzero.excel.util.ReflectUtil.listReadMethods;
import static org.ttzero.excel.util.ReflectUtil.readMethodsMap;
import static org.ttzero.excel.util.StringUtil.EMPTY;
import static org.ttzero.excel.util.StringUtil.isEmpty;
import static org.ttzero.excel.util.StringUtil.isNotEmpty;
/**
* 对象数组工作表,内部使用{@code List}做数据源,所以它是应用最广泛的一种工作表。
* {@code ListSheet}默认支持数据切片,少量数据可以在实例化时一次性传入,数据量较大时建议切片获取数据
*
*
* new Workbook("11月待拜访客户")
* .addSheet(new ListSheet<Customer>() {
* @Override
* protected List<Customer> more() {
* // 分页查询数据
* List<Customer> list = customerService.list(queryVo);
* // 页码 + 1
* queryVo.setPageNum(queryVo.getPageNum() + 1);
* return list;
* }
* }).writeTo(response.getOutputStream());
*
* 如上示例覆写{@link #more}方法获取切片数据,直到返回空数据或{@code null}为止,这样不至少将大量数据堆积到内存,
* 输出协议使用{@link RowBlock}进行装填数据并落盘。{@code more}方法在{@code ListSheet}工作表是一定会被
* 调用的即使初始化工作表传入了数据,工作表判断无需导出的情况除外,比如未指定表头且{@code Bean}对象无任何@ExcelColumn注释,
* 则会导出空工作表
*
* {@code ListSheet}使用{@link #getTClass}方法获取泛型的实际类型,内部优先使用{@link Class#getGenericSuperclass}方法获取,
* 如果有子类指定{@code T}类型则可以获取到{@code T}的类型,否则将使用数组中第一条数据做为泛型的具体类型,
* 如果希望无数据时依然导出表头就必须让{@code ListSheet}获取泛型{@code T}的实际类型,当无数据且无子类指定{@code T}时可以使用
* {@link #setClass}方法设置泛型的类型
*
* 大多数使用{@code ListSheet}工作表导数据时都会使用注解,{@code ListSheet}支持自定义注解以延申功能,
* 你甚至可以不使用任何的内置注释全部使用自定义注解来实现导入导出,使用自定义注解时需要搭配自定义{@code ListSheet}解析注解,
* 其中最重要的两个方法{@link #ignoreColumn(AccessibleObject)}和{@link #createColumn(AccessibleObject)}就是读取
* 方法或字段上的注解来创建{@code Column}对象,{@code AccessibleObject}可能是一个{@code Method}或者一个{@code Field}
*
* 对象取值会优先调用get方法获取,如果未发现get方法则直接从{@code field}取值。导出数据并不仅限于get方法,
* 你可以在任何无参且仅有一个返回值的方法上添加@ExcelColumn注解进行导出,你还可以在子类定义相同的方法来替换父类上的@ExcelColumn注解,
* 解析注解时会从子类往上逐级解析至到父级为{@code Object}为止
*
* 除子类覆写{@link #more}方法外还可以通过{@link #setData(BiFunction)}设置一个数据生产者,它可以减化数据分片的代码。
* {@code dataSupplier}被定义为{@code BiFunction>},其中第一个入参{@code Integer}表示已拉取数据的记录数
* (并非已写入数据),第二个入参{@code T}表示上一批数据中最后一个对象,业务端可以通过这两个参数来计算下一批数据应该从哪个节点开始拉取,
* 通常你可以使用第一个参数除以每批拉取的数据大小来确定当前页码,如果数据已排序则可以使用{@code T}对象的排序字段来计算下一批数据的游标从而跳过
* {@code limit ... offset ... }分页查询从而极大提升取数性能
*
*
* new Workbook()
* .addSheet(new ListSheet<Customer>()
* // 分页查询,每页查询100条数据,可以通过已拉取记录数计算当前页面
* .setData((i, lastOne) -> customerService.pagingQuery(i/100, 100))
* ).writeTo(Paths.get("f://abc.xlsx"));
*
* 参考文档:
*
*
* @author guanquan.wang at 2018-01-26 14:48
* @see ListMapSheet
* @see SimpleSheet
*/
public class ListSheet extends Sheet {
/**
* 临时存放数据
*/
protected List data;
/**
* 控制读取开始和结束下标
*/
protected int start, end;
/**
* 结束标记{@code EOF}
*/
protected boolean eof;
/**
* 泛型<T>的实际类型
*/
protected Class> tClazz;
/**
* 行级动态样式处理器
*/
protected StyleProcessor styleProcessor;
/**
* 强制导出,忽略安全限制全字段导出,确认需求后谨慎使用
*/
protected int forceExport;
/**
* 数据产生者,简化分片查询
*/
protected BiFunction> dataSupplier;
/**
* 设置行级动态样式处理器,作用于整行优先级高于单元格动态样式处理器
*
* @param styleProcessor 行级动态样式处理器
* @return 当前工作表
*/
public Sheet setStyleProcessor(StyleProcessor styleProcessor) {
this.styleProcessor = styleProcessor;
putExtProp(Const.ExtendPropertyKey.STYLE_DESIGN, styleProcessor);
return this;
}
/**
* 获取当前工作表的行级动态样式处理器,如果未设置则从扩展参数中查找
*
* @return 行级动态样式处理器
*/
public StyleProcessor getStyleProcessor() {
if (styleProcessor != null) return styleProcessor;
@SuppressWarnings("unchecked")
StyleProcessor fromExtProp = (StyleProcessor) getExtPropValue(Const.ExtendPropertyKey.STYLE_DESIGN);
return this.styleProcessor = fromExtProp;
}
/**
* 实例化工作表,未指定工作表名称时默认以{@code 'Sheet'+id}命名
*/
public ListSheet() {
super();
}
/**
* 实例化工作表并指定工作表名称
*
* @param name 工作表名称
*/
public ListSheet(String name) {
super(name);
}
/**
* 实例化工作表并指定表头信息
*
* @param columns 表头信息
*/
public ListSheet(final Column... columns) {
super(columns);
}
/**
* 实例化工作表并指定工作表名称和表头信息
*
* @param name 工作表名称
* @param columns 表头信息
*/
public ListSheet(String name, final Column... columns) {
super(name, columns);
}
/**
* 实例化工作表并指定工作表名称,水印和表头信息
*
* @param name 工作表名称
* @param waterMark 水印
* @param columns 表头信息
*/
public ListSheet(String name, WaterMark waterMark, final Column... columns) {
super(name, waterMark, columns);
}
/**
* 实例化工作表并指定初始数据
*
* @param data 初始数据
*/
public ListSheet(List data) {
this(null, data);
}
/**
* 实例化工作表并指定工作表名称和初始数据
*
* @param name 工作表名称
* @param data 初始数据
*/
public ListSheet(String name, List data) {
super(name);
setData(data);
}
/**
* 实例化工作表并指定初始数据和表头
*
* @param data 初始数据
* @param columns 表头信息
*/
public ListSheet(List data, final Column... columns) {
this(null, data, columns);
}
/**
* 实例化工作表并指定工作表名称、初始数据和表头
*
* @param name 工作表名称
* @param data 初始数据
* @param columns 表头信息
*/
public ListSheet(String name, List data, final Column... columns) {
this(name, data, null, columns);
}
/**
* 实例化工作表并指定初始数据、水印和表头
*
* @param data 初始数据
* @param waterMark 水印
* @param columns 表头信息
*/
public ListSheet(List data, WaterMark waterMark, final Column... columns) {
this(null, data, waterMark, columns);
}
/**
* 实例化工作表并指定工作表名称、初始数据、水印和表头
*
* @param name 工作表名称
* @param data 初始数据
* @param waterMark 水印
* @param columns 表头信息
*/
public ListSheet(String name, List data, WaterMark waterMark, final Column... columns) {
super(name, waterMark, columns);
setData(data);
}
/**
* 指定泛型{@code T}的实际类型,不指定时默认由反射或数组中第一个对象类型而定
*
* @param tClazz 泛型{@code T}的实际类型
* @return 当前工作表
*/
public Sheet setClass(Class> tClazz) {
this.tClazz = tClazz;
return this;
}
/**
* 设置初始数据,导出的时候依然会调用{@link #more()} 方法以获取更多数据
*
* @param data 初始数据
* @return 当前工作表
*/
public ListSheet setData(final List data) {
if (data == null) return this;
this.data = new ArrayList<>(data);
// Has data and worksheet can write
// Paging in advance
if (sheetWriter != null) {
paging();
}
return this;
}
/**
* 设置数据生产者,如果设置了此值{@link #more}方法将从此生产者中获取数据
*
* @param dataSupplier 数据生产者其中{@code Integer}为已拉取数据的记录数,{@code T}为上一批数据中最后一个对象
* @return 当前工作表
*/
public ListSheet setData(BiFunction> dataSupplier) {
this.dataSupplier = dataSupplier;
return this;
}
/**
* 获取队列中第一个非{@code null}对象用于解析
*
* @return 第一个非 {@code null}对象
*/
protected T getFirst() {
// 初始没有数据时调用一次more方法获取数据
if (data == null || data.isEmpty()) {
List more = more();
if (more != null && !more.isEmpty()) data = new ArrayList<>(more);
else return null;
}
T first = data.get(start);
if (first != null) return first;
int i = start + 1;
do {
first = data.get(i++);
} while (first == null && i< this.data.size());
return first;
}
/**
* Release resources
*
* @throws IOException if I/O error occur
*/
@Override
public void close() throws IOException {
// Maybe there has more data
if (!eof && rows >= getRowLimit()) {
List list = more();
if (list != null && !list.isEmpty()) {
compact();
data.addAll(list);
@SuppressWarnings("unchecked")
ListSheet copy = getClass().cast(clone());
copy.start = 0;
copy.end = list.size();
workbook.insertSheet(id, copy);
// Do not close current worksheet
shouldClose = false;
}
}
if (shouldClose && data != null) {
// Some Collection not support #remove
// data.clear();
data = null;
}
super.close();
}
/**
* 重置{@code RowBlock}行块数据
*/
@Override
protected void resetBlockData() {
if (!eof && left() < rowBlock.capacity()) {
append();
}
// Find the end index of row-block
int end = getEndIndex(), len = columns.length;
boolean hasGlobalStyleProcessor = (extPropMark & 2) == 2;
try {
for (; start < end; rows++, start++) {
Row row = rowBlock.next();
row.index = rows;
Cell[] cells = row.realloc(len);
T o = data.get(start);
boolean isNull = o == null;
for (int i = 0; i < len; i++) {
// Clear cells
Cell cell = cells[i];
cell.clear();
Object e;
EntryColumn column = (EntryColumn) columns[i];
/*
The default processing of null values still retains the row style.
If you don't want any style and value, you can change it to {@code continue}
*/
if (column.isIgnoreValue() || isNull)
e = null;
else {
if (column.getMethod() != null)
e = column.getMethod().invoke(o);
else if (column.getField() != null)
e = column.getField().get(o);
else e = o;
}
cellValueAndStyle.reset(row, cell, e, column);
if (hasGlobalStyleProcessor) {
cellValueAndStyle.setStyleDesign(o, cell, column, getStyleProcessor());
}
}
row.height = getRowHeight();
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new ExcelWriteException(e);
}
}
/**
* 加载数据,内部调用{@link #more}获取数据并判断是否需要分页,超过工作表行上限则调用{@link #paging}分页
*/
protected void append() {
int rbs = rowBlock.capacity();
for (; ; ) {
List list = more();
// No more data
if (list == null || list.isEmpty()) {
eof = shouldClose = true;
break;
}
// The first getting
if (data == null) {
setData(list);
if (list.size() < rbs) continue;
else break;
}
compact();
data.addAll(list);
start = 0;
end = data.size();
// Split worksheet
if (end >= rbs) {
paging();
break;
}
}
}
private void compact() {
// Copy the remaining data to a temporary array
int size = left();
if (start > 0 && size > 0) {
// append and resize
List last = new ArrayList<>(size);
last.addAll(data.subList(start, end));
data.clear();
data.addAll(last);
} else if (start > 0) data.clear();
}
/**
* 获取泛型T的实际类型,优先使用{@link Class#getGenericSuperclass}方法获取,如果有子类指定{@code T}类型则可以获取,
* 否则将使用数组中第一条数据做为泛型的具体类型
*
* @return T的实际类型,无法获取具体类型时返回{@code null}
*/
protected Class> getTClass() {
Class> clazz = tClazz;
if (clazz != null) return clazz;
if (getClass().getGenericSuperclass() instanceof ParameterizedType) {
Type type = ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
if (type instanceof Class) {
clazz = (Class) type;
}
}
if (clazz == null) {
T o = getFirst();
if (o == null) return null;
clazz = o.getClass();
}
tClazz = clazz;
return clazz;
}
/**
* 初始化表头信息,如果未指定{@code Columns}则默认反射{@code T}及其父类的所有字段,
* 并采集所有标记有{@link ExcelColumn}注解的字段和方法(这里限制无参数且仅有一个返回值的方法),
* {@code Column}顺序由{@code colIndex}决定,如果没有{@code colIndex}则按字段和方法在
* Java Bean中的定义顺序而定。
*
* 如果有指定{@code Columns}则忽略排序仅将{@link Column#key}与字段和方法进行绑定方便后续取值
*
* @return 表头列个数
*/
protected int init() {
Class> clazz = getTClass();
if (clazz == null) return columns != null ? columns.length : 0;
Map tmp = new HashMap<>();
try {
tmp.putAll(readMethodsMap(clazz, Object.class));
} catch (IntrospectionException e) {
LOGGER.warn("Get class {} methods failed.", clazz);
}
Field[] declaredFields = listDeclaredFieldsUntilJavaPackage(clazz, c -> !ignoreColumn(c));
boolean forceExport = this.forceExport == 1;
if (!hasHeaderColumns()) {
// Get ExcelColumn annotation method
List list = new ArrayList<>(declaredFields.length);
Map existsMethod = new HashMap<>(declaredFields.length);
for (int i = 0; i < declaredFields.length; i++) {
Field field = declaredFields[i];
field.setAccessible(true);
String gs = field.getName();
// Ignore annotation on read method
Method method = tmp.get(gs);
if (method != null) {
existsMethod.put(gs, method);
// Filter all ignore column
if (ignoreColumn(method)) {
declaredFields[i] = null;
continue;
}
EntryColumn column = createColumn(method);
// Force export
if (column == null && forceExport) {
column = new EntryColumn(gs, EMPTY, false);
}
if (column != null) {
EntryColumn tail = (EntryColumn) column.getTail();
tail.method = method;
tail.field = field;
tail.clazz = method.getReturnType();
tail.key = gs;
if (isEmpty(tail.name)) {
tail.name = gs;
}
list.add(column);
// Attach header style
buildHeaderStyle(method, field, tail);
// Attach header comment
buildHeaderComment(method, field, tail);
continue;
}
}
EntryColumn column = createColumn(field);
// Force export
if (column == null && forceExport) {
column = new EntryColumn(gs, EMPTY, false);
}
if (column != null) {
list.add(column);
EntryColumn tail = (EntryColumn) column.getTail();
tail.field = field;
tail.key = gs;
if (isEmpty(tail.name)) {
tail.name = gs;
}
if (method != null) {
tail.clazz = method.getReturnType();
tail.method = method;
} else tail.clazz = field.getType();
// Attach header style
buildHeaderStyle(method, field, tail);
// Attach header comment
buildHeaderComment(method, field, tail);
}
}
// Attach some custom column
List attachList = attachOtherColumn(existsMethod, clazz);
if (attachList != null) list.addAll(attachList);
// No column to write
if (list.isEmpty()) {
// 如果没有列可写则判断是否为简单类型,简单类型可直接输出
if (cellValueAndStyle.isAllowDirectOutput(clazz)) {
list.add(new EntryColumn().setClazz(clazz));
} else {
headerReady = eof = shouldClose = true;
this.end = 0;
if (java.util.Map.class.isAssignableFrom(clazz))
LOGGER.warn("List