diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java index fb743c8f1..d3e4be1ae 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java @@ -535,6 +535,7 @@ private String prepareData( return null; } StringBuilder preparedData = new StringBuilder(); + StringBuilder rawPrepareData = new StringBuilder(); AnalysisCell analysisCell = null; int startIndex = 0; @@ -553,17 +554,39 @@ private String prepareData( continue; } } + startIndex = prefixIndex + 1; int suffixIndex = -1; - while (suffixIndex == -1 && startIndex < length) { - suffixIndex = value.indexOf(FILL_SUFFIX, startIndex + 1); - if (suffixIndex < 0) { - break out; + boolean escapedSuffixSeen = false; + while (startIndex < length) { + int candidateIndex = value.indexOf(FILL_SUFFIX, startIndex); + if (candidateIndex < 0) { + break; } - startIndex = suffixIndex + 1; - char prefixSuffixChar = value.charAt(suffixIndex - 1); - if (prefixSuffixChar == IGNORE_CHAR) { - suffixIndex = -1; + startIndex = candidateIndex + 1; + if (value.charAt(candidateIndex - 1) != IGNORE_CHAR) { + suffixIndex = candidateIndex; + break; } + escapedSuffixSeen = true; + int nextPrefixIndex = value.indexOf(FILL_PREFIX, startIndex); + int nextSuffixIndex = value.indexOf(FILL_SUFFIX, startIndex); + if (nextPrefixIndex >= 0 && (nextSuffixIndex < 0 || nextPrefixIndex < nextSuffixIndex)) { + // The opener has no closing brace of its own: keep it and resume at the next placeholder. + rawPrepareData.append(convertPrepareData(value.substring(lastPrepareDataIndex, prefixIndex))); + rawPrepareData.append(value, prefixIndex, candidateIndex + 1); + lastPrepareDataIndex = candidateIndex + 1; + startIndex = lastPrepareDataIndex; + continue out; + } + } + if (suffixIndex < 0) { + if (escapedSuffixSeen) { + // Only escaped closing braces remain in the cell: keep the rest literal. + rawPrepareData.append(convertPrepareData(value.substring(lastPrepareDataIndex, prefixIndex))); + rawPrepareData.append(value.substring(prefixIndex)); + lastPrepareDataIndex = length; + } + break out; } if (analysisCell == null) { analysisCell = initAnalysisCell(rowIndex, columnIndex); @@ -585,12 +608,23 @@ private String prepareData( } analysisCell.getVariableList().add(variable); if (lastPrepareDataIndex == prefixIndex) { - analysisCell.getPrepareDataList().add(StringUtils.EMPTY); - if (lastPrepareDataIndex != 0) { + if (rawPrepareData.length() > 0) { + String data = rawPrepareData.toString(); + rawPrepareData.setLength(0); + preparedData.append(data); + analysisCell.getPrepareDataList().add(data); analysisCell.setOnlyOneVariable(Boolean.FALSE); + } else { + analysisCell.getPrepareDataList().add(StringUtils.EMPTY); + if (lastPrepareDataIndex != 0) { + analysisCell.setOnlyOneVariable(Boolean.FALSE); + } } } else { - String data = convertPrepareData(value.substring(lastPrepareDataIndex, prefixIndex)); + String data = rawPrepareData + .append(convertPrepareData(value.substring(lastPrepareDataIndex, prefixIndex))) + .toString(); + rawPrepareData.setLength(0); preparedData.append(data); analysisCell.getPrepareDataList().add(data); analysisCell.setOnlyOneVariable(Boolean.FALSE); @@ -603,7 +637,14 @@ private String prepareData( cell.setBlank(); } return dealAnalysisCell( - analysisCell, value, rowIndex, lastPrepareDataIndex, length, firstRowCache, preparedData); + analysisCell, + value, + rowIndex, + lastPrepareDataIndex, + length, + firstRowCache, + preparedData, + rawPrepareData); } private String dealAnalysisCell( @@ -613,12 +654,21 @@ private String dealAnalysisCell( int lastPrepareDataIndex, int length, Map> firstRowCache, - StringBuilder preparedData) { + StringBuilder preparedData, + StringBuilder rawPrepareData) { if (analysisCell != null) { if (lastPrepareDataIndex == length) { - analysisCell.getPrepareDataList().add(StringUtils.EMPTY); + if (rawPrepareData.length() > 0) { + analysisCell.getPrepareDataList().add(rawPrepareData.toString()); + analysisCell.setOnlyOneVariable(Boolean.FALSE); + } else { + analysisCell.getPrepareDataList().add(StringUtils.EMPTY); + } } else { - analysisCell.getPrepareDataList().add(convertPrepareData(value.substring(lastPrepareDataIndex))); + String data = rawPrepareData + .append(convertPrepareData(value.substring(lastPrepareDataIndex))) + .toString(); + analysisCell.getPrepareDataList().add(data); analysisCell.setOnlyOneVariable(Boolean.FALSE); } UniqueDataFlagKey uniqueDataFlag = diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java new file mode 100644 index 000000000..afd05b0da --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fesod.sheet.fill; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Regression tests for issue #462: a template placeholder whose closing brace is escaped (for + * example {@code {foo\}}) has no valid closing brace to parse, so the text must be kept as-is + * instead of breaking the fill. + */ +@Tag(Tags.ROUND_TRIP) +public class EscapedPlaceholderTest extends AbstractExcelTest { + + private static final String TEMPLATE_SHEET_NAME = "Sheet1"; + private static final String VARIABLE = "foo"; + private static final String VALUE = "replaced_value"; + + @Test + void escapedClosingBraceIsKeptAsLiteralText() throws IOException { + // {foo\} used to blow up with StringIndexOutOfBoundsException while parsing the suffix + Assertions.assertEquals("{foo\\}", fillTemplateWith("{foo\\}")); + } + + @Test + void escapedOpeningBraceIsKeptAsLiteralText() throws IOException { + Assertions.assertEquals("\\{foo}", fillTemplateWith("\\{foo}")); + } + + @Test + void escapedPlaceholderInsideTextIsKeptAsLiteralText() throws IOException { + Assertions.assertEquals("prefix {foo\\} suffix", fillTemplateWith("prefix {foo\\} suffix")); + } + + @Test + void normalPlaceholderIsStillReplaced() throws IOException { + Assertions.assertEquals(VALUE, fillTemplateWith("{foo}")); + } + + @Test + void escapedPlaceholderBeforeNormalPlaceholderIsHandledIndependently() throws IOException { + Assertions.assertEquals("{foo\\} second_value", fillTemplateWith("{foo\\} {bar}")); + } + + /** + * Fills a single-cell template holding {@code cellText} and returns the filled value read back + * from the output file. + */ + private String fillTemplateWith(String cellText) throws IOException { + File template = createTemplate(cellText); + File output = createTempFile("escaped-placeholder-out", ExcelFormat.XLSX); + Map data = new HashMap<>(); + data.put(VARIABLE, VALUE); + data.put("bar", "second_value"); + FesodSheet.write(output).withTemplate(template).sheet().doFill(data); + List> rows = + FesodSheet.read(output).sheet().headRowNumber(0).doReadSync(); + return rows.get(0).get(0); + } + + private File createTemplate(String cellText) throws IOException { + File template = createTempFile("escaped-placeholder", ExcelFormat.XLSX); + try (Workbook workbook = new XSSFWorkbook(); + FileOutputStream fos = new FileOutputStream(template)) { + Sheet sheet = workbook.createSheet(TEMPLATE_SHEET_NAME); + Row row = sheet.createRow(0); + row.createCell(0).setCellValue(cellText); + workbook.write(fos); + } + return template; + } +}