diff --git a/fastexcel-test/src/test/java/cn/idev/excel/test/demo/fill/FillTest.java b/fastexcel-test/src/test/java/cn/idev/excel/test/demo/fill/FillTest.java index 27fa66616..a0e11ab1d 100644 --- a/fastexcel-test/src/test/java/cn/idev/excel/test/demo/fill/FillTest.java +++ b/fastexcel-test/src/test/java/cn/idev/excel/test/demo/fill/FillTest.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.junit.jupiter.api.Test; /** @@ -260,6 +261,43 @@ public void dateFormatFill() { EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(data()); } + /** + * Example for filling a unconventional template. + */ + @Test + public void unconventionalFill() { + String templateFileName = + TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "unconventional.xlsx"; + String fileName = TestFileUtil.getPath() + "unconventionalFill" + System.currentTimeMillis() + ".xlsx"; + + try (ExcelWriter excelWriter = + EasyExcel.write(fileName).withTemplate(templateFileName).build()) { + Map map = MapUtils.newHashMap(); + map.put("foo", "foo-value"); + map.put("bar", "bar-value"); + String[] sheetNames = {"merge", "multi", "escape", "empty"}; + for (String sheetName : sheetNames) { + excelWriter.fill(map, EasyExcel.writerSheet(sheetName).build()); + } + + Function>> getListMap = size -> { + List> listMap = ListUtils.newArrayListWithCapacity(size); + for (int j = 0; j < size; j++) { + Map currentMap = MapUtils.newHashMap(); + currentMap.put("bar1", "Bar1-" + j); + currentMap.put("bar2", "Bar2-" + j); + currentMap.put("bar3", "Bar3-" + j); + currentMap.put("bar4", "Bar4-" + j); + currentMap.put("bar5", "Bar5-" + j); + listMap.add(currentMap); + } + return listMap; + }; + excelWriter.fill(map, EasyExcel.writerSheet("list").build()); + excelWriter.fill(getListMap.apply(10), EasyExcel.writerSheet("list").build()); + } + } + private List data() { List list = ListUtils.newArrayList(); for (int i = 0; i < 10; i++) { diff --git a/fastexcel-test/src/test/resources/demo/fill/unconventional.xlsx b/fastexcel-test/src/test/resources/demo/fill/unconventional.xlsx new file mode 100644 index 000000000..1366671c3 Binary files /dev/null and b/fastexcel-test/src/test/resources/demo/fill/unconventional.xlsx differ diff --git a/fastexcel/src/main/java/cn/idev/excel/enums/TemplateStringPartType.java b/fastexcel/src/main/java/cn/idev/excel/enums/TemplateStringPartType.java new file mode 100644 index 000000000..1b0302717 --- /dev/null +++ b/fastexcel/src/main/java/cn/idev/excel/enums/TemplateStringPartType.java @@ -0,0 +1,21 @@ +package cn.idev.excel.enums; + +/** + * template string part type + */ +public enum TemplateStringPartType { + /** + * Content in plain text. + */ + TEXT, + + /** + * Common variable. + */ + COMMON_VARIABLE, + + /** + * Collection variable. + */ + COLLECTION_VARIABLE +} diff --git a/fastexcel/src/main/java/cn/idev/excel/write/executor/ExcelWriteFillExecutor.java b/fastexcel/src/main/java/cn/idev/excel/write/executor/ExcelWriteFillExecutor.java index 94c1ffd00..36410ef8d 100644 --- a/fastexcel/src/main/java/cn/idev/excel/write/executor/ExcelWriteFillExecutor.java +++ b/fastexcel/src/main/java/cn/idev/excel/write/executor/ExcelWriteFillExecutor.java @@ -2,6 +2,7 @@ import cn.idev.excel.context.WriteContext; import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.enums.TemplateStringPartType; import cn.idev.excel.enums.WriteDirectionEnum; import cn.idev.excel.enums.WriteTemplateAnalysisCellTypeEnum; import cn.idev.excel.exception.ExcelGenerateException; @@ -15,22 +16,16 @@ import cn.idev.excel.util.PoiUtils; import cn.idev.excel.util.StringUtils; import cn.idev.excel.util.WriteHandlerUtils; +import cn.idev.excel.write.handler.TemplateStringParseHandler; import cn.idev.excel.write.handler.context.CellWriteHandlerContext; import cn.idev.excel.write.handler.context.RowWriteHandlerContext; import cn.idev.excel.write.metadata.fill.AnalysisCell; import cn.idev.excel.write.metadata.fill.FillConfig; import cn.idev.excel.write.metadata.fill.FillWrapper; +import cn.idev.excel.write.metadata.fill.TemplateStringPart; import cn.idev.excel.write.metadata.holder.WriteSheetHolder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -44,17 +39,8 @@ /** * Fill the data into excel - * - * */ public class ExcelWriteFillExecutor extends AbstractExcelWriteExecutor { - - private static final String ESCAPE_FILL_PREFIX = "\\\\\\{"; - private static final String ESCAPE_FILL_SUFFIX = "\\\\\\}"; - private static final String FILL_PREFIX = "{"; - private static final String FILL_SUFFIX = "}"; - private static final char IGNORE_CHAR = '\\'; - private static final String COLLECTION_PREFIX = "."; /** * Fields to replace in the template */ @@ -83,6 +69,11 @@ public class ExcelWriteFillExecutor extends AbstractExcelWriteExecutor { */ private UniqueDataFlagKey currentUniqueDataFlag; + /** + * The template string parse handler for this fill + */ + private TemplateStringParseHandler currentTemplateStringParseHandler; + public ExcelWriteFillExecutor(WriteContext writeContext) { super(writeContext); } @@ -109,6 +100,7 @@ public void fill(Object data, FillConfig fillConfig) { currentDataPrefix = null; } currentUniqueDataFlag = uniqueDataFlag(writeContext.writeSheetHolder(), currentDataPrefix); + currentTemplateStringParseHandler = fillConfig.getTemplateStringParseHandler(); // processing data if (realData instanceof Collection) { @@ -510,95 +502,91 @@ private String prepareData( if (StringUtils.isEmpty(value)) { return null; } - StringBuilder preparedData = new StringBuilder(); - AnalysisCell analysisCell = null; - - int startIndex = 0; - int length = value.length(); - int lastPrepareDataIndex = 0; - out: - while (startIndex < length) { - int prefixIndex = value.indexOf(FILL_PREFIX, startIndex); - if (prefixIndex < 0) { - break; + Collection templateStringParts = currentTemplateStringParseHandler.parse(value); + if (CollectionUtils.isEmpty(templateStringParts)) { + return null; + } + AnalysisCell analysisCell = analysisByTemplateStringParts(value, rowIndex, columnIndex, templateStringParts); + if (analysisCell != null) { + List variableList = analysisCell.getVariableList(), + prepareDataList = analysisCell.getPrepareDataList(); + // fix https://github.com/fast-excel/fastexcel/issues/1552 + // When read template, XLSX data may be in `is` labels, and set the time set in `v` label, lead to can't set + // up successfully, so all data format to empty first. + if (CollectionUtils.isNotEmpty(variableList)) { + cell.setBlank(); + } else if (CollectionUtils.isNotEmpty(prepareDataList)) { + return String.join(StringUtils.EMPTY, analysisCell.getPrepareDataList()); } - if (prefixIndex != 0) { - char prefixPrefixChar = value.charAt(prefixIndex - 1); - if (prefixPrefixChar == IGNORE_CHAR) { - startIndex = prefixIndex + 1; + } + return dealAnalysisCell(analysisCell, rowIndex, firstRowCache); + } + + private AnalysisCell analysisByTemplateStringParts( + String value, Integer rowIndex, Integer columnIndex, Collection templateStringParts) { + List orderedTemplateStringParts = templateStringParts.stream() + .filter(part -> part != null && part.getType() != null) + .sorted(Comparator.comparing(TemplateStringPart::getOrder, Comparator.nullsLast(Integer::compareTo))) + .collect(Collectors.toList()); + AnalysisCell analysisCell = null; + StringBuilder textAppender = new StringBuilder(); + int partsSize = orderedTemplateStringParts.size(); + for (int i = 0; i < partsSize; i++) { + TemplateStringPart templateStringPart = orderedTemplateStringParts.get(i); + TemplateStringPartType currentPartType = templateStringPart.getType(); + boolean tail = i == partsSize - 1; + if (TemplateStringPartType.TEXT == currentPartType) { + textAppender.append(templateStringPart.getText()); + if (!tail) { continue; } - } - int suffixIndex = -1; - while (suffixIndex == -1 && startIndex < length) { - suffixIndex = value.indexOf(FILL_SUFFIX, startIndex + 1); - if (suffixIndex < 0) { - break out; + String trailingText = textAppender.toString(); + if (analysisCell == null) { + if (!trailingText.equals(value)) { + AnalysisCell cell = new AnalysisCell(); + cell.setPrepareDataList(Collections.singletonList(trailingText)); + return cell; + } + continue; } - startIndex = suffixIndex + 1; - char prefixSuffixChar = value.charAt(suffixIndex - 1); - if (prefixSuffixChar == IGNORE_CHAR) { - suffixIndex = -1; + analysisCell.getPrepareDataList().add(trailingText); + if (Boolean.TRUE.equals(analysisCell.getOnlyOneVariable()) && !StringUtils.isEmpty(trailingText)) { + analysisCell.setOnlyOneVariable(Boolean.FALSE); } + continue; } if (analysisCell == null) { analysisCell = initAnalysisCell(rowIndex, columnIndex); } - String variable = value.substring(prefixIndex + 1, suffixIndex); - if (StringUtils.isEmpty(variable)) { - continue; + String previousText = textAppender.toString(); + analysisCell.getPrepareDataList().add(previousText); + if (textAppender.length() > 0) { + textAppender.setLength(0); } - int collectPrefixIndex = variable.indexOf(COLLECTION_PREFIX); - if (collectPrefixIndex > -1) { - if (collectPrefixIndex != 0) { - analysisCell.setPrefix(variable.substring(0, collectPrefixIndex)); - } - variable = variable.substring(collectPrefixIndex + 1); - if (StringUtils.isEmpty(variable)) { - continue; - } - analysisCell.setCellType(WriteTemplateAnalysisCellTypeEnum.COLLECTION); + if (tail) { + analysisCell.getPrepareDataList().add(textAppender.toString()); } - analysisCell.getVariableList().add(variable); - if (lastPrepareDataIndex == prefixIndex) { - analysisCell.getPrepareDataList().add(StringUtils.EMPTY); - // fix https://github.com/fast-excel/fastexcel/issues/2035 - if (lastPrepareDataIndex != 0) { - analysisCell.setOnlyOneVariable(Boolean.FALSE); - } - } else { - String data = convertPrepareData(value.substring(lastPrepareDataIndex, prefixIndex)); - preparedData.append(data); - analysisCell.getPrepareDataList().add(data); + List variableList = analysisCell.getVariableList(); + if (Boolean.TRUE.equals(analysisCell.getOnlyOneVariable()) + && (!variableList.isEmpty() || !StringUtils.isEmpty(previousText))) { analysisCell.setOnlyOneVariable(Boolean.FALSE); } - lastPrepareDataIndex = suffixIndex + 1; - } - // fix https://github.com/fast-excel/fastexcel/issues/1552 - // When read template, XLSX data may be in `is` labels, and set the time set in `v` label, lead to can't set - // up successfully, so all data format to empty first. - if (analysisCell != null && CollectionUtils.isNotEmpty(analysisCell.getVariableList())) { - cell.setBlank(); + if (TemplateStringPartType.COMMON_VARIABLE.equals(currentPartType)) { + variableList.add(templateStringPart.getVariableName()); + analysisCell.setCellType(WriteTemplateAnalysisCellTypeEnum.COMMON); + continue; + } + variableList.add(templateStringPart.getVariableName()); + analysisCell.setCellType(WriteTemplateAnalysisCellTypeEnum.COLLECTION); + String collectionName = templateStringPart.getCollectionName(); + analysisCell.setPrefix(StringUtils.isEmpty(collectionName) ? null : collectionName); } - return dealAnalysisCell( - analysisCell, value, rowIndex, lastPrepareDataIndex, length, firstRowCache, preparedData); + return analysisCell; } private String dealAnalysisCell( - AnalysisCell analysisCell, - String value, - int rowIndex, - int lastPrepareDataIndex, - int length, - Map> firstRowCache, - StringBuilder preparedData) { + AnalysisCell analysisCell, int rowIndex, Map> firstRowCache) { if (analysisCell != null) { - if (lastPrepareDataIndex == length) { - analysisCell.getPrepareDataList().add(StringUtils.EMPTY); - } else { - analysisCell.getPrepareDataList().add(convertPrepareData(value.substring(lastPrepareDataIndex))); - analysisCell.setOnlyOneVariable(Boolean.FALSE); - } UniqueDataFlagKey uniqueDataFlag = uniqueDataFlag(writeContext.writeSheetHolder(), analysisCell.getPrefix()); if (WriteTemplateAnalysisCellTypeEnum.COMMON.equals(analysisCell.getCellType())) { @@ -619,7 +607,12 @@ private String dealAnalysisCell( collectionAnalysisCellList.add(analysisCell); } - return preparedData.toString(); + List prepareDataList = analysisCell.getPrepareDataList(); + return String.join( + StringUtils.EMPTY, + prepareDataList.size() > 1 + ? prepareDataList.subList(0, prepareDataList.size() - 1) + : prepareDataList); } return null; } @@ -638,12 +631,6 @@ private AnalysisCell initAnalysisCell(Integer rowIndex, Integer columnIndex) { return analysisCell; } - private String convertPrepareData(String prepareData) { - prepareData = prepareData.replaceAll(ESCAPE_FILL_PREFIX, FILL_PREFIX); - prepareData = prepareData.replaceAll(ESCAPE_FILL_SUFFIX, FILL_SUFFIX); - return prepareData; - } - private UniqueDataFlagKey uniqueDataFlag(WriteSheetHolder writeSheetHolder, String wrapperName) { return new UniqueDataFlagKey(writeSheetHolder.getSheetNo(), writeSheetHolder.getSheetName(), wrapperName); } diff --git a/fastexcel/src/main/java/cn/idev/excel/write/handler/TemplateStringParseHandler.java b/fastexcel/src/main/java/cn/idev/excel/write/handler/TemplateStringParseHandler.java new file mode 100644 index 000000000..4883a293f --- /dev/null +++ b/fastexcel/src/main/java/cn/idev/excel/write/handler/TemplateStringParseHandler.java @@ -0,0 +1,17 @@ +package cn.idev.excel.write.handler; + +import cn.idev.excel.write.metadata.fill.TemplateStringPart; +import java.util.Collection; + +/** + * Template string parse handler + */ +public interface TemplateStringParseHandler { + /** + * Parse the template string + * + * @param stringValue String value + * @return The multi parts formed after parsing a template string + */ + Collection parse(String stringValue); +} diff --git a/fastexcel/src/main/java/cn/idev/excel/write/handler/impl/DefaultTemplateStringParseHandler.java b/fastexcel/src/main/java/cn/idev/excel/write/handler/impl/DefaultTemplateStringParseHandler.java new file mode 100644 index 000000000..74ee0dd8f --- /dev/null +++ b/fastexcel/src/main/java/cn/idev/excel/write/handler/impl/DefaultTemplateStringParseHandler.java @@ -0,0 +1,111 @@ +package cn.idev.excel.write.handler.impl; + +import cn.idev.excel.util.StringUtils; +import cn.idev.excel.write.handler.TemplateStringParseHandler; +import cn.idev.excel.write.metadata.fill.TemplateStringPart; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Default template string parse handler + */ +public class DefaultTemplateStringParseHandler implements TemplateStringParseHandler { + private static final String ESCAPE_FILL_PREFIX = "\\\\\\{"; + private static final String ESCAPE_FILL_SUFFIX = "\\\\\\}"; + private static final String FILL_PREFIX = "{"; + private static final String FILL_SUFFIX = "}"; + private static final char IGNORE_CHAR = '\\'; + private static final String COLLECTION_PREFIX = "."; + + private static final List EMPTY_PART_LIST = Collections.emptyList(); + private static volatile DefaultTemplateStringParseHandler instance; + + public static synchronized DefaultTemplateStringParseHandler getInstance() { + if (instance == null) { + synchronized (DefaultTemplateStringParseHandler.class) { + if (instance == null) { + instance = new DefaultTemplateStringParseHandler(); + } + } + } + return instance; + } + + @Override + public Collection parse(String value) { + if (StringUtils.isEmpty(value)) { + return EMPTY_PART_LIST; + } + List partList = new ArrayList<>(); + int length = value.length(); + int startIndex = 0, lastPartIndex = 0; + out: + while (startIndex < length) { + int prefixIndex = value.indexOf(FILL_PREFIX, startIndex); + if (prefixIndex < 0) { + break; + } + startIndex = prefixIndex + 1; + if (prefixIndex != 0) { + char prefixPrefixChar = value.charAt(prefixIndex - 1); + if (prefixPrefixChar == IGNORE_CHAR) { + continue; + } + } + int suffixIndex = -1; + while (suffixIndex == -1) { + if (startIndex >= length) { + break out; + } + suffixIndex = value.indexOf(FILL_SUFFIX, startIndex); + if (suffixIndex < 0) { + break out; + } + startIndex = suffixIndex + 1; + char prefixSuffixChar = value.charAt(suffixIndex - 1); + if (prefixSuffixChar == IGNORE_CHAR) { + suffixIndex = -1; + } + } + String variable = value.substring(prefixIndex + 1, suffixIndex); + if (StringUtils.isEmpty(variable)) { + continue; + } + // Add the text part in the gap between the current variable and the previous variable + TemplateStringPart variableGapPart = lastPartIndex == prefixIndex + ? TemplateStringPart.emptyText() + : TemplateStringPart.text(processEscape(value.substring(lastPartIndex, prefixIndex))); + lastPartIndex = suffixIndex + 1; + partList.add(variableGapPart); + // Add the current variable part + int collectPrefixIndex = variable.indexOf(COLLECTION_PREFIX); + if (collectPrefixIndex < 0) { + partList.add(TemplateStringPart.commonVariable(variable)); + continue; + } + String truncatedVariable = variable.substring(collectPrefixIndex + 1); + // In order to adapt to the original filling effect + // The symbol If there is no actual variable after, it will be regarded as a common variable + if (StringUtils.isEmpty(truncatedVariable)) { + partList.add(TemplateStringPart.commonVariable(variable)); + continue; + } + String collectionName = collectPrefixIndex == 0 ? null : variable.substring(0, collectPrefixIndex); + partList.add(TemplateStringPart.collectionVariable(collectionName, truncatedVariable)); + } + // Add the trailing text part + TemplateStringPart trailingPart = lastPartIndex == length + ? TemplateStringPart.emptyText() + : TemplateStringPart.text(processEscape(value.substring(lastPartIndex))); + partList.add(trailingPart); + return partList; + } + + private String processEscape(String stringValue) { + stringValue = stringValue.replaceAll(ESCAPE_FILL_PREFIX, FILL_PREFIX); + stringValue = stringValue.replaceAll(ESCAPE_FILL_SUFFIX, FILL_SUFFIX); + return stringValue; + } +} diff --git a/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/FillConfig.java b/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/FillConfig.java index 45b18db86..69568988d 100644 --- a/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/FillConfig.java +++ b/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/FillConfig.java @@ -1,6 +1,8 @@ package cn.idev.excel.write.metadata.fill; import cn.idev.excel.enums.WriteDirectionEnum; +import cn.idev.excel.write.handler.TemplateStringParseHandler; +import cn.idev.excel.write.handler.impl.DefaultTemplateStringParseHandler; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -36,6 +38,11 @@ public class FillConfig { */ private Boolean autoStyle; + /** + * template string parse handler + */ + private TemplateStringParseHandler templateStringParseHandler; + private boolean hasInit; public void init() { @@ -51,6 +58,9 @@ public void init() { if (autoStyle == null) { autoStyle = Boolean.TRUE; } + if (templateStringParseHandler == null) { + templateStringParseHandler = DefaultTemplateStringParseHandler.getInstance(); + } hasInit = true; } } diff --git a/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/TemplateStringPart.java b/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/TemplateStringPart.java new file mode 100644 index 000000000..71ed7a9ae --- /dev/null +++ b/fastexcel/src/main/java/cn/idev/excel/write/metadata/fill/TemplateStringPart.java @@ -0,0 +1,82 @@ +package cn.idev.excel.write.metadata.fill; + +import cn.idev.excel.enums.TemplateStringPartType; +import cn.idev.excel.util.StringUtils; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@EqualsAndHashCode +public class TemplateStringPart { + /** + * Empty text part. + */ + public static final TemplateStringPart EMPTY_TEXT_PART = text(StringUtils.EMPTY); + + /** + * Represents the type of this part. + * + * @see TemplateStringPartType + */ + private TemplateStringPartType type; + + /** + * Represents a textual value and only has meaning if this partial type is text. + */ + private String text; + + /** + * Represents variable name and only has meaning if this partial type is variable. + */ + private String variableName; + + /** + * Represents collection name and only has meaning if this partial type is collection variable. + */ + private String collectionName; + + /** + * Represents the order of this part before parsing the original string, and the order of null values is later. + */ + private Integer order; + + public static TemplateStringPart text(String text) { + if (text == null) { + throw new IllegalArgumentException("The text parameter cannot be empty when creating a text part"); + } + return new TemplateStringPart(TemplateStringPartType.TEXT, text, null, null, null); + } + + public static TemplateStringPart commonVariable(String variableName) { + if (variableName == null) { + throw new IllegalArgumentException( + "The variableName parameter cannot be null " + "when creating a variable part"); + } + return new TemplateStringPart(TemplateStringPartType.COMMON_VARIABLE, null, variableName, null, null); + } + + public static TemplateStringPart collectionVariable(String collectionName, String variableName) { + if (variableName == null) { + throw new IllegalArgumentException( + "The variableName parameter cannot be null " + "when creating a collection variable part"); + } + return new TemplateStringPart( + TemplateStringPartType.COLLECTION_VARIABLE, null, variableName, collectionName, null); + } + + public static TemplateStringPart bareCollectionVariable(String variableName) { + return collectionVariable(null, variableName); + } + + public static TemplateStringPart emptyText() { + return EMPTY_TEXT_PART; + } + + public TemplateStringPart order(Integer order) { + this.order = order; + return this; + } +}