From e4d21545724ab6978e1c60a9470a4b04eccc006b Mon Sep 17 00:00:00 2001 From: Roeniss Moon Date: Tue, 13 Oct 2026 14:21:29 +0800 Subject: [PATCH 1/4] fix: handle escaped closing brace in template placeholder parsing --- .../fesod/sheet/write/executor/ExcelWriteFillExecutor.java | 3 +++ 1 file changed, 3 insertions(+) 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..fd281df7f 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 @@ -565,6 +565,9 @@ private String prepareData( suffixIndex = -1; } } + if (suffixIndex < 0) { + break out; + } if (analysisCell == null) { analysisCell = initAnalysisCell(rowIndex, columnIndex); } From 4661e87416fbbb50d3b794164b616bc8b665edb8 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:25:05 +0800 Subject: [PATCH 2/4] test: add regression tests for escaped placeholder parsing (#462) --- .../sheet/fill/EscapedPlaceholderTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java 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..1f084d340 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java @@ -0,0 +1,99 @@ +/* + * 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}")); + } + + /** + * 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); + 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; + } +} From e894d9bc7815cc5480d1cbc355bb984da84fbcd4 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:41:57 +0800 Subject: [PATCH 3/4] test: cover escaped placeholder before a normal placeholder (#462) --- .../org/apache/fesod/sheet/fill/EscapedPlaceholderTest.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 1f084d340..afd05b0da 100644 --- 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 @@ -70,6 +70,11 @@ 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. @@ -79,6 +84,7 @@ private String fillTemplateWith(String cellText) throws IOException { 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(); From bbe472d4f6032787b33b05b53401d23dc79fd9f2 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:41:57 +0800 Subject: [PATCH 4/4] fix: resume parsing at the next placeholder after an escaped brace (#462) --- .../executor/ExcelWriteFillExecutor.java | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) 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 fd281df7f..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,19 +554,38 @@ 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) { @@ -588,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); @@ -606,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( @@ -616,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 =