From 4c1c4f05253ba859712ead24e2d6ccbe3a74b7e4 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 23 Sep 2026 18:55:58 +0300 Subject: [PATCH 01/17] fix: never leave plaintext when encryption fails --- .../fesod/sheet/context/WriteContextImpl.java | 8 +++ .../sheet/readwrite/EncryptDataTest.java | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index fd118305d..01ac3a796 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -542,6 +542,7 @@ public void finish(boolean onException) { isOutputStreamEncrypt = doOutputStreamEncrypt07(); } catch (Throwable t) { throwable = t; + writeExcel = false; } } if (!isOutputStreamEncrypt) { @@ -574,6 +575,13 @@ public void finish(boolean onException) { doFileEncrypt07(); } catch (Throwable t) { throwable = t; + // The workbook was written to the file before encryption, so the file still holds the unprotected + // workbook and must not be left behind. + File file = writeWorkbookHolder.getFile(); + if (file.exists() && !file.delete()) { + throwable = + new ExcelGenerateException("Can not delete unencrypted file: " + file.getAbsolutePath(), t); + } } } try { diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java index 1eb375ee8..2ea6ddad4 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java @@ -25,10 +25,12 @@ package org.apache.fesod.sheet.readwrite; +import java.io.ByteArrayOutputStream; import java.io.File; import java.nio.file.Files; import java.util.List; import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.exception.ExcelGenerateException; import org.apache.fesod.sheet.read.builder.ExcelReaderBuilder; import org.apache.fesod.sheet.support.ExcelTypeEnum; import org.apache.fesod.sheet.testkit.Tags; @@ -38,9 +40,13 @@ import org.apache.fesod.sheet.testkit.listeners.CollectingReadListener; import org.apache.fesod.sheet.testkit.models.SimpleData; import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.apache.fesod.sheet.util.FileUtils; import org.apache.fesod.sheet.write.builder.ExcelWriterBuilder; +import org.apache.fesod.sheet.write.handler.WorkbookWriteHandler; +import org.apache.fesod.sheet.write.handler.context.WorkbookWriteHandlerContext; import org.apache.poi.EncryptedDocumentException; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -138,4 +144,53 @@ void xlsPasswordWrite_isActuallyEncrypted() throws Exception { .doReadSync(); Assertions.assertEquals(10, dataList.size()); } + + /** + * Verifies that when encrypting an XLSX stream write fails, nothing is written to the stream rather than + * the unencrypted workbook. + */ + @Test + void xlsxStreamPasswordWrite_encryptionFails_writesNothing() throws Exception { + // A temp directory under a regular file cannot be created, so the encryption step fails + File regularFile = createTempFile("enc-blocker", ExcelFormat.XLSX); + String originalPrefix = FileUtils.getTempFilePrefix(); + FileUtils.setTempFilePrefix(regularFile.getAbsolutePath() + File.separator + "sub" + File.separator); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Assertions.assertThrows(ExcelGenerateException.class, () -> FesodSheet.write(out, SimpleData.class) + .excelType(ExcelTypeEnum.XLSX) + .password(PASSWORD) + .sheet() + .doWrite(TestDataBuilder.simpleData(10))); + Assertions.assertEquals(0, out.size()); + } finally { + FileUtils.setTempFilePrefix(originalPrefix); + } + } + + /** + * Verifies that when encrypting an XLSX file write fails, the unencrypted file is not left behind. + */ + @Test + void xlsxFilePasswordWrite_encryptionFails_leavesNoFile() throws Exception { + File file = createTempFile("enc-readonly", ExcelFormat.XLSX); + // Root ignores file permissions, so the failure forced below cannot happen + Assumptions.assumeTrue(file.setReadOnly() && !file.canWrite(), "file permissions are not enforced"); + Assertions.assertTrue(file.setWritable(true)); + // Once the write stream is open, making the file read-only makes the encrypt-in-place step fail + WorkbookWriteHandler makeReadOnly = new WorkbookWriteHandler() { + @Override + public void afterWorkbookDispose(WorkbookWriteHandlerContext context) { + Assertions.assertTrue(file.setReadOnly()); + } + }; + + Assertions.assertThrows(ExcelGenerateException.class, () -> FesodSheet.write(file, SimpleData.class) + .excelType(ExcelTypeEnum.XLSX) + .password(PASSWORD) + .registerWriteHandler(makeReadOnly) + .sheet() + .doWrite(TestDataBuilder.simpleData(10))); + Assertions.assertFalse(file.exists()); + } } From bea52211b59f486fa2b0eff704c9667b6f5c706e Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 00:36:55 +0300 Subject: [PATCH 02/17] fix: keep the first failure when finishing a write --- .../fesod/sheet/context/WriteContextImpl.java | 20 ++++-- .../sheet/context/WriteContextImplTest.java | 61 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/context/WriteContextImplTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 01ac3a796..19fb9db7d 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -552,7 +552,7 @@ public void finish(boolean onException) { } writeWorkbookHolder.getWorkbook().close(); } catch (Throwable t) { - throwable = t; + throwable = keepFirstFailure(throwable, t); } } try { @@ -561,27 +561,28 @@ public void finish(boolean onException) { ((SXSSFWorkbook) workbook).dispose(); } } catch (Throwable t) { - throwable = t; + throwable = keepFirstFailure(throwable, t); } try { if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) { writeWorkbookHolder.getOutputStream().close(); } } catch (Throwable t) { - throwable = t; + throwable = keepFirstFailure(throwable, t); } if (writeExcel && !isOutputStreamEncrypt) { try { doFileEncrypt07(); } catch (Throwable t) { - throwable = t; + Throwable failure = t; // The workbook was written to the file before encryption, so the file still holds the unprotected // workbook and must not be left behind. File file = writeWorkbookHolder.getFile(); if (file.exists() && !file.delete()) { - throwable = + failure = new ExcelGenerateException("Can not delete unencrypted file: " + file.getAbsolutePath(), t); } + throwable = keepFirstFailure(throwable, failure); } } try { @@ -589,7 +590,7 @@ public void finish(boolean onException) { writeWorkbookHolder.getTempTemplateInputStream().close(); } } catch (Throwable t) { - throwable = t; + throwable = keepFirstFailure(throwable, t); } clearEncrypt03(); removeThreadLocalCache(); @@ -601,6 +602,13 @@ public void finish(boolean onException) { } } + /** + * Returns the failure already recorded, or the new one if there is none yet. + */ + private static Throwable keepFirstFailure(Throwable recorded, Throwable t) { + return recorded != null ? recorded : t; + } + /** * Removes thread-local caches used during the write process. */ diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/context/WriteContextImplTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/context/WriteContextImplTest.java new file mode 100644 index 000000000..617528cb1 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/context/WriteContextImplTest.java @@ -0,0 +1,61 @@ +/* + * 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.context; + +import java.io.IOException; +import java.io.OutputStream; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.exception.ExcelGenerateException; +import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.builders.TestDataBuilder; +import org.apache.fesod.sheet.testkit.models.SimpleData; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(Tags.WRITE) +class WriteContextImplTest { + + /** + * When writing the workbook fails and closing the stream then fails too, the write failure is the cause. + */ + @Test + void finish_writeAndCloseFail_reportsWriteFailure() { + OutputStream brokenStream = new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("write failed"); + } + + @Override + public void close() throws IOException { + throw new IOException("close failed"); + } + }; + + ExcelGenerateException e = Assertions.assertThrows( + ExcelGenerateException.class, () -> FesodSheet.write(brokenStream, SimpleData.class) + .excelType(ExcelTypeEnum.XLSX) + .sheet() + .doWrite(TestDataBuilder.simpleData(10))); + Assertions.assertEquals("write failed", e.getCause().getMessage()); + } +} From f2e3b8933740e3e4dcbcde07c9775a303a75fc44 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 01:01:35 +0300 Subject: [PATCH 03/17] fix: delete the temp file when its write fails --- .../fesod/sheet/context/WriteContextImpl.java | 15 +++---- .../sheet/readwrite/EncryptDataTest.java | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 19fb9db7d..2c0531f57 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -687,20 +687,15 @@ private boolean doOutputStreamEncrypt07() throws Exception { File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx); try { - writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); - } finally { try { + writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); + } finally { writeWorkbookHolder.getWorkbook().close(); tempFileOutputStream.close(); - } catch (Exception e) { - if (!tempXlsx.delete()) { - throw new ExcelGenerateException("Can not delete temp File!"); - } - throw e; } - } - try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(tempXlsx)) { - fileSystem.writeFilesystem(writeWorkbookHolder.getOutputStream()); + try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(tempXlsx)) { + fileSystem.writeFilesystem(writeWorkbookHolder.getOutputStream()); + } } finally { if (!tempXlsx.delete()) { throw new ExcelGenerateException("Can not delete temp File!"); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java index 2ea6ddad4..4c3f9e939 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java @@ -27,6 +27,8 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.util.List; import org.apache.fesod.sheet.FesodSheet; @@ -45,6 +47,7 @@ import org.apache.fesod.sheet.write.handler.WorkbookWriteHandler; import org.apache.fesod.sheet.write.handler.context.WorkbookWriteHandlerContext; import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; @@ -168,6 +171,43 @@ void xlsxStreamPasswordWrite_encryptionFails_writesNothing() throws Exception { } } + /** + * Verifies that when writing the unencrypted temp file for an XLSX stream write fails, the temp file is deleted. + */ + @Test + void xlsxStreamPasswordWrite_tempFileWriteFails_deletesTempFile() { + File tempFileDir = new File(tempDir, "fesod-temp"); + String originalPrefix = FileUtils.getTempFilePrefix(); + FileUtils.setTempFilePrefix(tempFileDir.getAbsolutePath() + File.separator); + // Swaps in a workbook whose write puts the data out and then fails, like a disk filling up + WorkbookWriteHandler failingWorkbook = new WorkbookWriteHandler() { + @Override + public void afterWorkbookDispose(WorkbookWriteHandlerContext context) { + SXSSFWorkbook workbook = new SXSSFWorkbook() { + @Override + public void write(OutputStream stream) throws IOException { + super.write(stream); + throw new IOException("write failed"); + } + }; + workbook.createSheet("s").createRow(0).createCell(0).setCellValue("secret"); + context.getWriteWorkbookHolder().setWorkbook(workbook); + } + }; + try { + Assertions.assertThrows( + ExcelGenerateException.class, () -> FesodSheet.write(new ByteArrayOutputStream(), SimpleData.class) + .excelType(ExcelTypeEnum.XLSX) + .password(PASSWORD) + .registerWriteHandler(failingWorkbook) + .sheet("s") + .doWrite(TestDataBuilder.simpleData(10))); + Assertions.assertArrayEquals(new String[0], tempFileDir.list()); + } finally { + FileUtils.setTempFilePrefix(originalPrefix); + } + } + /** * Verifies that when encrypting an XLSX file write fails, the unencrypted file is not left behind. */ From 79304681c71603aef60b9a475028034b4483f680 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 30 Sep 2026 11:49:31 +0300 Subject: [PATCH 04/17] refactor: use try-with-resources --- .../fesod/sheet/context/WriteContextImpl.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 2c0531f57..57470391c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -685,19 +685,19 @@ private boolean doOutputStreamEncrypt07() throws Exception { return false; } File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); - FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx); try { - try { - writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); - } finally { - writeWorkbookHolder.getWorkbook().close(); - tempFileOutputStream.close(); + try (FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx)) { + try { + writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); + } finally { + writeWorkbookHolder.getWorkbook().close(); + } } try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(tempXlsx)) { fileSystem.writeFilesystem(writeWorkbookHolder.getOutputStream()); } } finally { - if (!tempXlsx.delete()) { + if (tempXlsx.exists() && !tempXlsx.delete()) { throw new ExcelGenerateException("Can not delete temp File!"); } } From 32e196036e05c4ad718fb5af88c15b765626f33e Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 30 Sep 2026 13:32:13 +0300 Subject: [PATCH 05/17] test: add cases for handling temp file cleanup during failures --- .../sheet/readwrite/EncryptDataTest.java | 126 +++++++++++++++--- 1 file changed, 107 insertions(+), 19 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java index 4c3f9e939..2222db2f4 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/EncryptDataTest.java @@ -27,10 +27,12 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.util.List; +import org.apache.commons.io.output.BrokenOutputStream; import org.apache.fesod.sheet.FesodSheet; import org.apache.fesod.sheet.exception.ExcelGenerateException; import org.apache.fesod.sheet.read.builder.ExcelReaderBuilder; @@ -177,32 +179,118 @@ void xlsxStreamPasswordWrite_encryptionFails_writesNothing() throws Exception { @Test void xlsxStreamPasswordWrite_tempFileWriteFails_deletesTempFile() { File tempFileDir = new File(tempDir, "fesod-temp"); - String originalPrefix = FileUtils.getTempFilePrefix(); - FileUtils.setTempFilePrefix(tempFileDir.getAbsolutePath() + File.separator); - // Swaps in a workbook whose write puts the data out and then fails, like a disk filling up - WorkbookWriteHandler failingWorkbook = new WorkbookWriteHandler() { + WorkbookWriteHandler failingWorkbook = swapWorkbook(new SXSSFWorkbook() { + @Override + public void write(OutputStream stream) throws IOException { + super.write(stream); + throw new IOException("write failed"); + } + }); + Assertions.assertThrows( + ExcelGenerateException.class, + () -> writeWithPassword(tempFileDir, new ByteArrayOutputStream(), failingWorkbook)); + Assertions.assertArrayEquals(new String[0], tempFileDir.list()); + } + + /** + * Verifies that when closing the workbook after writing the temp file fails, the temp file is deleted. + */ + @Test + void xlsxStreamPasswordWrite_workbookCloseFails_deletesTempFile() { + File tempFileDir = new File(tempDir, "fesod-temp"); + WorkbookWriteHandler failingWorkbook = swapWorkbook(new SXSSFWorkbook() { + @Override + public void close() throws IOException { + super.close(); + throw new IOException("close failed"); + } + }); + Assertions.assertThrows( + ExcelGenerateException.class, + () -> writeWithPassword(tempFileDir, new ByteArrayOutputStream(), failingWorkbook)); + Assertions.assertArrayEquals(new String[0], tempFileDir.list()); + } + + /** + * Verifies that when closing the workbook after writing the temp file fails, the temp file stream is closed. + */ + @Test + void xlsxStreamPasswordWrite_workbookCloseFails_closesTempFileStream() { + File tempFileDir = new File(tempDir, "fesod-temp"); + OutputStream[] tempFileStream = new OutputStream[1]; + WorkbookWriteHandler failingWorkbook = swapWorkbook(new SXSSFWorkbook() { + @Override + public void write(OutputStream stream) throws IOException { + if (tempFileStream[0] == null) { + tempFileStream[0] = stream; + } + super.write(stream); + } + + @Override + public void close() throws IOException { + super.close(); + throw new IOException("close failed"); + } + }); + Assertions.assertThrows( + ExcelGenerateException.class, + () -> writeWithPassword(tempFileDir, new ByteArrayOutputStream(), failingWorkbook)); + Assertions.assertThrows(IOException.class, () -> tempFileStream[0].write(0)); + } + + /** + * Verifies that when writing the encrypted workbook to the caller's stream fails, the temp file is deleted. + */ + @Test + void xlsxStreamPasswordWrite_encryptedWriteFails_deletesTempFile() { + File tempFileDir = new File(tempDir, "fesod-temp"); + Assertions.assertThrows( + ExcelGenerateException.class, () -> writeWithPassword(tempFileDir, BrokenOutputStream.INSTANCE, null)); + Assertions.assertArrayEquals(new String[0], tempFileDir.list()); + } + + /** + * Verifies that when the temp file cannot be opened, the open error is reported rather than a failed delete. + */ + @Test + void xlsxStreamPasswordWrite_tempFileOpenFails_reportsOpenError() { + File tempFileDir = new File(tempDir, "fesod-temp"); + Assertions.assertTrue(tempFileDir.mkdirs()); + Assertions.assertTrue(tempFileDir.setWritable(false)); + try { + // Root and some file systems ignore the flag, so the open would not fail there + Assumptions.assumeFalse(tempFileDir.canWrite()); + ExcelGenerateException e = Assertions.assertThrows( + ExcelGenerateException.class, + () -> writeWithPassword(tempFileDir, new ByteArrayOutputStream(), null)); + Assertions.assertInstanceOf(FileNotFoundException.class, e.getCause()); + } finally { + Assertions.assertTrue(tempFileDir.setWritable(true)); + } + } + + private static WorkbookWriteHandler swapWorkbook(SXSSFWorkbook workbook) { + return new WorkbookWriteHandler() { @Override public void afterWorkbookDispose(WorkbookWriteHandlerContext context) { - SXSSFWorkbook workbook = new SXSSFWorkbook() { - @Override - public void write(OutputStream stream) throws IOException { - super.write(stream); - throw new IOException("write failed"); - } - }; workbook.createSheet("s").createRow(0).createCell(0).setCellValue("secret"); context.getWriteWorkbookHolder().setWorkbook(workbook); } }; + } + + private void writeWithPassword(File tempFileDir, OutputStream outputStream, WorkbookWriteHandler writeHandler) { + String originalPrefix = FileUtils.getTempFilePrefix(); + FileUtils.setTempFilePrefix(tempFileDir.getAbsolutePath() + File.separator); try { - Assertions.assertThrows( - ExcelGenerateException.class, () -> FesodSheet.write(new ByteArrayOutputStream(), SimpleData.class) - .excelType(ExcelTypeEnum.XLSX) - .password(PASSWORD) - .registerWriteHandler(failingWorkbook) - .sheet("s") - .doWrite(TestDataBuilder.simpleData(10))); - Assertions.assertArrayEquals(new String[0], tempFileDir.list()); + ExcelWriterBuilder writerBuilder = FesodSheet.write(outputStream, SimpleData.class) + .excelType(ExcelTypeEnum.XLSX) + .password(PASSWORD); + if (writeHandler != null) { + writerBuilder.registerWriteHandler(writeHandler); + } + writerBuilder.sheet("s").doWrite(TestDataBuilder.simpleData(10)); } finally { FileUtils.setTempFilePrefix(originalPrefix); } From 36945ebffd3d7112ba070c37362f5b0a025419ca Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 01:22:16 +0300 Subject: [PATCH 06/17] refactor: extract the password-type check --- .../fesod/sheet/context/WriteContextImpl.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 57470391c..c84b5c0dc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -663,13 +663,20 @@ public Workbook getWorkbook() { * Clears encryption settings for older Excel formats. */ private void clearEncrypt03() { - if (StringUtils.isEmpty(writeWorkbookHolder.getPassword()) - || !ExcelTypeEnum.XLS.equals(writeWorkbookHolder.getExcelType())) { + if (!isEncrypted(ExcelTypeEnum.XLS)) { return; } Biff8EncryptionKey.setCurrentUserPassword(null); } + /** + * Checks whether the workbook is password-protected and written as the given type. + */ + private boolean isEncrypted(ExcelTypeEnum excelType) { + return !StringUtils.isEmpty(writeWorkbookHolder.getPassword()) + && excelType.equals(writeWorkbookHolder.getExcelType()); + } + /** * Encrypts the output stream for newer Excel formats. * @@ -677,8 +684,7 @@ private void clearEncrypt03() { * @throws Exception If an error occurs during encryption. */ private boolean doOutputStreamEncrypt07() throws Exception { - if (StringUtils.isEmpty(writeWorkbookHolder.getPassword()) - || !ExcelTypeEnum.XLSX.equals(writeWorkbookHolder.getExcelType())) { + if (!isEncrypted(ExcelTypeEnum.XLSX)) { return false; } if (writeWorkbookHolder.getFile() != null) { @@ -709,8 +715,7 @@ private boolean doOutputStreamEncrypt07() throws Exception { */ private void doFileEncrypt07() throws Exception { // Check if the password is empty or the file type is not xlsx, if so, return directly - if (StringUtils.isEmpty(writeWorkbookHolder.getPassword()) - || !ExcelTypeEnum.XLSX.equals(writeWorkbookHolder.getExcelType())) { + if (!isEncrypted(ExcelTypeEnum.XLSX)) { return; } // Check if the file is null, if so, return directly From 586d428fb7a61b7a36e94abc2c91683d3d977c5f Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 01:23:06 +0300 Subject: [PATCH 07/17] refactor: split finish() into named steps --- .../fesod/sheet/context/WriteContextImpl.java | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index c84b5c0dc..76a0ca907 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.util.List; import java.util.Map; @@ -496,8 +497,22 @@ public void finish(boolean onException) { // executes the callback after all sheets has been fully written. boolean shouldSkip = onException && !writeWorkbookHolder.getWriteExcelOnException(); - Map writeSheetHolderMap = - shouldSkip ? null : writeWorkbookHolder.getHasBeenInitializedSheetIndexMap(); + if (!shouldSkip) { + afterSheetsDispose(); + } + + WriteHandlerUtils.afterWorkbookDispose(writeWorkbookHolder.getWorkbookWriteHandlerContext()); + if (writeWorkbookHolder == null) { + return; + } + writeAndCloseWorkbook(onException); + } + + /** + * Runs the after-sheet-dispose handlers for every initialized sheet. + */ + private void afterSheetsDispose() { + Map writeSheetHolderMap = writeWorkbookHolder.getHasBeenInitializedSheetIndexMap(); if (MapUtils.isNotEmpty(writeSheetHolderMap)) { if (MapUtils.size(writeSheetHolderMap) == 1) { SheetWriteHandlerContext sheetWriteHandlerContext = @@ -524,11 +539,15 @@ public void finish(boolean onException) { } } } + } - WriteHandlerUtils.afterWorkbookDispose(writeWorkbookHolder.getWorkbookWriteHandlerContext()); - if (writeWorkbookHolder == null) { - return; - } + /** + * Writes the workbook, encrypted if a password is set, then closes the workbook and its streams. Every step + * runs even if an earlier one failed, and the first failure is thrown at the end. + * + * @param onException Indicates whether the finish is triggered by an exception. + */ + private void writeAndCloseWorkbook(boolean onException) { Throwable throwable = null; boolean isOutputStreamEncrypt = false; // Determine if you need to write excel @@ -547,26 +566,18 @@ public void finish(boolean onException) { } if (!isOutputStreamEncrypt) { try { - if (writeExcel) { - writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); - } - writeWorkbookHolder.getWorkbook().close(); + writeWorkbook(writeExcel); } catch (Throwable t) { throwable = keepFirstFailure(throwable, t); } } try { - Workbook workbook = writeWorkbookHolder.getWorkbook(); - if (workbook instanceof SXSSFWorkbook) { - ((SXSSFWorkbook) workbook).dispose(); - } + disposeSxssfWorkbook(); } catch (Throwable t) { throwable = keepFirstFailure(throwable, t); } try { - if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) { - writeWorkbookHolder.getOutputStream().close(); - } + closeOutputStream(); } catch (Throwable t) { throwable = keepFirstFailure(throwable, t); } @@ -586,9 +597,7 @@ public void finish(boolean onException) { } } try { - if (writeWorkbookHolder.getTempTemplateInputStream() != null) { - writeWorkbookHolder.getTempTemplateInputStream().close(); - } + closeTemplateInputStream(); } catch (Throwable t) { throwable = keepFirstFailure(throwable, t); } @@ -602,6 +611,44 @@ public void finish(boolean onException) { } } + /** + * Writes the unencrypted workbook to the output stream if requested, then closes the workbook. + */ + private void writeWorkbook(boolean writeExcel) throws IOException { + if (writeExcel) { + writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); + } + writeWorkbookHolder.getWorkbook().close(); + } + + /** + * Deletes the temporary files backing a streaming workbook. + */ + private void disposeSxssfWorkbook() { + Workbook workbook = writeWorkbookHolder.getWorkbook(); + if (workbook instanceof SXSSFWorkbook) { + ((SXSSFWorkbook) workbook).dispose(); + } + } + + /** + * Closes the output stream, unless the caller asked to keep it open. + */ + private void closeOutputStream() throws IOException { + if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) { + writeWorkbookHolder.getOutputStream().close(); + } + } + + /** + * Closes the temporary copy of the template input stream, if one was made. + */ + private void closeTemplateInputStream() throws IOException { + if (writeWorkbookHolder.getTempTemplateInputStream() != null) { + writeWorkbookHolder.getTempTemplateInputStream().close(); + } + } + /** * Returns the failure already recorded, or the new one if there is none yet. */ From 72a0b16054308a088ee14c8d60840bcb062aef1d Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 01:23:42 +0300 Subject: [PATCH 08/17] refactor: run finish() steps through runStep --- .../fesod/sheet/context/WriteContextImpl.java | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 76a0ca907..2ddabe259 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -564,23 +564,12 @@ private void writeAndCloseWorkbook(boolean onException) { writeExcel = false; } } + boolean writePlainWorkbook = writeExcel; if (!isOutputStreamEncrypt) { - try { - writeWorkbook(writeExcel); - } catch (Throwable t) { - throwable = keepFirstFailure(throwable, t); - } - } - try { - disposeSxssfWorkbook(); - } catch (Throwable t) { - throwable = keepFirstFailure(throwable, t); - } - try { - closeOutputStream(); - } catch (Throwable t) { - throwable = keepFirstFailure(throwable, t); + throwable = runStep(throwable, () -> writeWorkbook(writePlainWorkbook)); } + throwable = runStep(throwable, this::disposeSxssfWorkbook); + throwable = runStep(throwable, this::closeOutputStream); if (writeExcel && !isOutputStreamEncrypt) { try { doFileEncrypt07(); @@ -596,11 +585,7 @@ private void writeAndCloseWorkbook(boolean onException) { throwable = keepFirstFailure(throwable, failure); } } - try { - closeTemplateInputStream(); - } catch (Throwable t) { - throwable = keepFirstFailure(throwable, t); - } + throwable = runStep(throwable, this::closeTemplateInputStream); clearEncrypt03(); removeThreadLocalCache(); if (throwable != null) { @@ -649,6 +634,24 @@ private void closeTemplateInputStream() throws IOException { } } + @FunctionalInterface + private interface FinishStep { + void run() throws Exception; + } + + /** + * Runs one step of the finishing sequence. A failure is recorded instead of stopping the sequence, and only the + * first failure is kept. + */ + private static Throwable runStep(Throwable throwable, FinishStep step) { + try { + step.run(); + } catch (Throwable t) { + return keepFirstFailure(throwable, t); + } + return throwable; + } + /** * Returns the failure already recorded, or the new one if there is none yet. */ From 84719881f71416ba6726321dce2aaf333a161522 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 24 Sep 2026 01:24:34 +0300 Subject: [PATCH 09/17] refactor: extract WorkbookEncryption --- .../sheet/context/WorkbookEncryption.java | 148 ++++++++++++++++++ .../fesod/sheet/context/WriteContextImpl.java | 110 +------------ 2 files changed, 152 insertions(+), 106 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java new file mode 100644 index 000000000..d1ffdad9b --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -0,0 +1,148 @@ +/* + * 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. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.context; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.UUID; +import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.exception.ExcelGenerateException; +import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.util.FileUtils; +import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionMode; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; + +/** + * Password protection applied when a workbook is finished: xlsx is encrypted into the output stream or the output + * file, and the xls password is cleared afterwards. + */ +final class WorkbookEncryption { + + private final WriteWorkbookHolder writeWorkbookHolder; + + WorkbookEncryption(WriteWorkbookHolder writeWorkbookHolder) { + this.writeWorkbookHolder = writeWorkbookHolder; + } + + /** + * Clears encryption settings for older Excel formats. + */ + void clearEncrypt03() { + if (!isEncrypted(ExcelTypeEnum.XLS)) { + return; + } + Biff8EncryptionKey.setCurrentUserPassword(null); + } + + /** + * Checks whether the workbook is password-protected and written as the given type. + */ + private boolean isEncrypted(ExcelTypeEnum excelType) { + return !StringUtils.isEmpty(writeWorkbookHolder.getPassword()) + && excelType.equals(writeWorkbookHolder.getExcelType()); + } + + /** + * Encrypts the output stream for newer Excel formats. + * + * @return True if encryption is successful, otherwise false. + * @throws Exception If an error occurs during encryption. + */ + boolean doOutputStreamEncrypt07() throws Exception { + if (!isEncrypted(ExcelTypeEnum.XLSX)) { + return false; + } + if (writeWorkbookHolder.getFile() != null) { + return false; + } + File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); + try { + try (FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx)) { + try { + writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); + } finally { + writeWorkbookHolder.getWorkbook().close(); + } + } + try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(tempXlsx)) { + fileSystem.writeFilesystem(writeWorkbookHolder.getOutputStream()); + } + } finally { + if (tempXlsx.exists() && !tempXlsx.delete()) { + throw new ExcelGenerateException("Can not delete temp File!"); + } + } + return true; + } + + /** + * To encrypt + */ + void doFileEncrypt07() throws Exception { + // Check if the password is empty or the file type is not xlsx, if so, return directly + if (!isEncrypted(ExcelTypeEnum.XLSX)) { + return; + } + // Check if the file is null, if so, return directly + if (writeWorkbookHolder.getFile() == null) { + return; + } + // Use try-with-resources to automatically close resources, encrypt and write the file + try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(writeWorkbookHolder.getFile()); + FileOutputStream fileOutputStream = new FileOutputStream(writeWorkbookHolder.getFile())) { + fileSystem.writeFilesystem(fileOutputStream); + } + } + + /** + * Opens a file system and encrypts the given file. + * + * This method creates a new POIFSFileSystem instance, sets up an Encryptor with a standard encryption mode, + * and confirms the password for encryption. It then opens the provided file in read-write mode, saves its content + * into an encrypted output stream, and finally returns the encrypted file system. + * + * @param file The file to be encrypted. + * @return An encrypted POIFSFileSystem object. + * @throws Exception If any error occurs during the encryption process or file handling. + */ + private POIFSFileSystem openFileSystemAndEncrypt(File file) throws Exception { + POIFSFileSystem fileSystem = new POIFSFileSystem(); + Encryptor encryptor = new EncryptionInfo(EncryptionMode.standard).getEncryptor(); + encryptor.confirmPassword(writeWorkbookHolder.getPassword()); + try (OPCPackage opcPackage = OPCPackage.open(file, PackageAccess.READ_WRITE); + OutputStream outputStream = encryptor.getDataStream(fileSystem)) { + opcPackage.save(outputStream); + } + return fileSystem; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 2ddabe259..8d2ded869 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -26,13 +26,11 @@ package org.apache.fesod.sheet.context; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; @@ -45,10 +43,8 @@ import org.apache.fesod.sheet.metadata.Head; import org.apache.fesod.sheet.metadata.data.WriteCellData; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; -import org.apache.fesod.sheet.support.ExcelTypeEnum; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.DateUtils; -import org.apache.fesod.sheet.util.FileUtils; import org.apache.fesod.sheet.util.NumberDataFormatterUtils; import org.apache.fesod.sheet.util.NumberUtils; import org.apache.fesod.sheet.util.WorkBookUtil; @@ -65,13 +61,6 @@ import org.apache.fesod.sheet.write.metadata.holder.WriteTableHolder; import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder; import org.apache.fesod.sheet.write.property.ExcelWriteHeadProperty; -import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; -import org.apache.poi.openxml4j.opc.OPCPackage; -import org.apache.poi.openxml4j.opc.PackageAccess; -import org.apache.poi.poifs.crypt.EncryptionInfo; -import org.apache.poi.poifs.crypt.EncryptionMode; -import org.apache.poi.poifs.crypt.Encryptor; -import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -548,6 +537,7 @@ private void afterSheetsDispose() { * @param onException Indicates whether the finish is triggered by an exception. */ private void writeAndCloseWorkbook(boolean onException) { + WorkbookEncryption encryption = new WorkbookEncryption(writeWorkbookHolder); Throwable throwable = null; boolean isOutputStreamEncrypt = false; // Determine if you need to write excel @@ -558,7 +548,7 @@ private void writeAndCloseWorkbook(boolean onException) { // No data is written if an exception is thrown if (writeExcel) { try { - isOutputStreamEncrypt = doOutputStreamEncrypt07(); + isOutputStreamEncrypt = encryption.doOutputStreamEncrypt07(); } catch (Throwable t) { throwable = t; writeExcel = false; @@ -572,7 +562,7 @@ private void writeAndCloseWorkbook(boolean onException) { throwable = runStep(throwable, this::closeOutputStream); if (writeExcel && !isOutputStreamEncrypt) { try { - doFileEncrypt07(); + encryption.doFileEncrypt07(); } catch (Throwable t) { Throwable failure = t; // The workbook was written to the file before encryption, so the file still holds the unprotected @@ -586,7 +576,7 @@ private void writeAndCloseWorkbook(boolean onException) { } } throwable = runStep(throwable, this::closeTemplateInputStream); - clearEncrypt03(); + encryption.clearEncrypt03(); removeThreadLocalCache(); if (throwable != null) { throw new ExcelGenerateException("Can not close IO.", throwable); @@ -708,96 +698,4 @@ public OutputStream getOutputStream() { public Workbook getWorkbook() { return writeWorkbookHolder.getWorkbook(); } - - /** - * Clears encryption settings for older Excel formats. - */ - private void clearEncrypt03() { - if (!isEncrypted(ExcelTypeEnum.XLS)) { - return; - } - Biff8EncryptionKey.setCurrentUserPassword(null); - } - - /** - * Checks whether the workbook is password-protected and written as the given type. - */ - private boolean isEncrypted(ExcelTypeEnum excelType) { - return !StringUtils.isEmpty(writeWorkbookHolder.getPassword()) - && excelType.equals(writeWorkbookHolder.getExcelType()); - } - - /** - * Encrypts the output stream for newer Excel formats. - * - * @return True if encryption is successful, otherwise false. - * @throws Exception If an error occurs during encryption. - */ - private boolean doOutputStreamEncrypt07() throws Exception { - if (!isEncrypted(ExcelTypeEnum.XLSX)) { - return false; - } - if (writeWorkbookHolder.getFile() != null) { - return false; - } - File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); - try { - try (FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx)) { - try { - writeWorkbookHolder.getWorkbook().write(tempFileOutputStream); - } finally { - writeWorkbookHolder.getWorkbook().close(); - } - } - try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(tempXlsx)) { - fileSystem.writeFilesystem(writeWorkbookHolder.getOutputStream()); - } - } finally { - if (tempXlsx.exists() && !tempXlsx.delete()) { - throw new ExcelGenerateException("Can not delete temp File!"); - } - } - return true; - } - - /** - * To encrypt - */ - private void doFileEncrypt07() throws Exception { - // Check if the password is empty or the file type is not xlsx, if so, return directly - if (!isEncrypted(ExcelTypeEnum.XLSX)) { - return; - } - // Check if the file is null, if so, return directly - if (writeWorkbookHolder.getFile() == null) { - return; - } - // Use try-with-resources to automatically close resources, encrypt and write the file - try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(writeWorkbookHolder.getFile()); - FileOutputStream fileOutputStream = new FileOutputStream(writeWorkbookHolder.getFile())) { - fileSystem.writeFilesystem(fileOutputStream); - } - } - - /** - * Opens a file system and encrypts the given file. - * - * This method creates a new POIFSFileSystem instance, sets up an Encryptor with a standard encryption mode, - * and confirms the password for encryption. It then opens the provided file in read-write mode, saves its content - * into an encrypted output stream, and finally returns the encrypted file system. - * - * @param file The file to be encrypted. - * @return An encrypted POIFSFileSystem object. - * @throws Exception If any error occurs during the encryption process or file handling. - */ - private POIFSFileSystem openFileSystemAndEncrypt(File file) throws Exception { - POIFSFileSystem fileSystem = new POIFSFileSystem(); - Encryptor encryptor = new EncryptionInfo(EncryptionMode.standard).getEncryptor(); - encryptor.confirmPassword(writeWorkbookHolder.getPassword()); - try (OPCPackage opcPackage = OPCPackage.open(file, PackageAccess.READ_WRITE); - OutputStream outputStream = encryptor.getDataStream(fileSystem)) { - opcPackage.save(outputStream); - } - return fileSystem; - } } From 44fd0f6d4a46bb4a66f94a4f08201ed5e53ffd45 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Thu, 8 Oct 2026 15:41:40 +0300 Subject: [PATCH 10/17] refactor: tidy the remaining finish() steps --- .../sheet/context/WorkbookEncryption.java | 18 +++++++--- .../fesod/sheet/context/WriteContextImpl.java | 35 ++++--------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java index d1ffdad9b..9e358625e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -117,10 +117,20 @@ void doFileEncrypt07() throws Exception { if (writeWorkbookHolder.getFile() == null) { return; } - // Use try-with-resources to automatically close resources, encrypt and write the file - try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(writeWorkbookHolder.getFile()); - FileOutputStream fileOutputStream = new FileOutputStream(writeWorkbookHolder.getFile())) { - fileSystem.writeFilesystem(fileOutputStream); + try { + // Use try-with-resources to automatically close resources, encrypt and write the file + try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(writeWorkbookHolder.getFile()); + FileOutputStream fileOutputStream = new FileOutputStream(writeWorkbookHolder.getFile())) { + fileSystem.writeFilesystem(fileOutputStream); + } + } catch (Throwable t) { + // The workbook was written to the file before encryption, so the file still holds the unprotected + // workbook and must not be left behind. + File file = writeWorkbookHolder.getFile(); + if (file.exists() && !file.delete()) { + throw new ExcelGenerateException("Can not delete unencrypted file: " + file.getAbsolutePath(), t); + } + throw t; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 8d2ded869..afd6cdf8f 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -25,7 +25,6 @@ package org.apache.fesod.sheet.context; -import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.List; @@ -485,15 +484,11 @@ public void finish(boolean onException) { finished = true; // executes the callback after all sheets has been fully written. - boolean shouldSkip = onException && !writeWorkbookHolder.getWriteExcelOnException(); - if (!shouldSkip) { + if (!onException || writeWorkbookHolder.getWriteExcelOnException()) { afterSheetsDispose(); } WriteHandlerUtils.afterWorkbookDispose(writeWorkbookHolder.getWorkbookWriteHandlerContext()); - if (writeWorkbookHolder == null) { - return; - } writeAndCloseWorkbook(onException); } @@ -531,8 +526,9 @@ private void afterSheetsDispose() { } /** - * Writes the workbook, encrypted if a password is set, then closes the workbook and its streams. Every step - * runs even if an earlier one failed, and the first failure is thrown at the end. + * Writes the workbook, encrypted if a password is set, then closes the workbook and its streams. If encrypting + * into the output stream fails, the unencrypted workbook is not written in its place. Each close step runs even + * if an earlier step failed, and the first failure is thrown at the end. * * @param onException Indicates whether the finish is triggered by an exception. */ @@ -561,19 +557,7 @@ private void writeAndCloseWorkbook(boolean onException) { throwable = runStep(throwable, this::disposeSxssfWorkbook); throwable = runStep(throwable, this::closeOutputStream); if (writeExcel && !isOutputStreamEncrypt) { - try { - encryption.doFileEncrypt07(); - } catch (Throwable t) { - Throwable failure = t; - // The workbook was written to the file before encryption, so the file still holds the unprotected - // workbook and must not be left behind. - File file = writeWorkbookHolder.getFile(); - if (file.exists() && !file.delete()) { - failure = - new ExcelGenerateException("Can not delete unencrypted file: " + file.getAbsolutePath(), t); - } - throwable = keepFirstFailure(throwable, failure); - } + throwable = runStep(throwable, encryption::doFileEncrypt07); } throwable = runStep(throwable, this::closeTemplateInputStream); encryption.clearEncrypt03(); @@ -637,18 +621,11 @@ private static Throwable runStep(Throwable throwable, FinishStep step) { try { step.run(); } catch (Throwable t) { - return keepFirstFailure(throwable, t); + return throwable != null ? throwable : t; } return throwable; } - /** - * Returns the failure already recorded, or the new one if there is none yet. - */ - private static Throwable keepFirstFailure(Throwable recorded, Throwable t) { - return recorded != null ? recorded : t; - } - /** * Removes thread-local caches used during the write process. */ From fde36d23ba519ac7de32ef56fa165bd251f2ec19 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 11 Oct 2026 20:17:22 +0300 Subject: [PATCH 11/17] refactor: add ExceptionCollector for improved error handling --- .../fesod/sheet/context/WriteContextImpl.java | 68 ++++------------ .../fesod/sheet/util/ExceptionCollector.java | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+), 52 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 1d588bb1c..3f306accc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; @@ -44,6 +45,7 @@ import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.DateUtils; +import org.apache.fesod.sheet.util.ExceptionCollector; import org.apache.fesod.sheet.util.NumberDataFormatterUtils; import org.apache.fesod.sheet.util.NumberUtils; import org.apache.fesod.sheet.util.WorkBookUtil; @@ -534,8 +536,8 @@ private void afterSheetsDispose() { */ private void writeAndCloseWorkbook(boolean onException) { WorkbookEncryption encryption = new WorkbookEncryption(writeWorkbookHolder); - Throwable throwable = null; - boolean isOutputStreamEncrypt = false; + ExceptionCollector collector = new ExceptionCollector(); + AtomicBoolean outputStreamEncrypted = new AtomicBoolean(false); // Determine if you need to write excel boolean writeExcel = !onException; if (writeWorkbookHolder.getWriteExcelOnException()) { @@ -543,28 +545,23 @@ private void writeAndCloseWorkbook(boolean onException) { } // No data is written if an exception is thrown if (writeExcel) { - try { - isOutputStreamEncrypt = encryption.doOutputStreamEncrypt07(); - } catch (Throwable t) { - throwable = recordFailure(throwable, t); + collector.run(() -> outputStreamEncrypted.set(encryption.doOutputStreamEncrypt07())); + if (collector.hasFailure()) { writeExcel = false; } } - boolean writePlainWorkbook = writeExcel; - if (!isOutputStreamEncrypt) { - throwable = runStep(throwable, () -> writeWorkbook(writePlainWorkbook)); + if (!outputStreamEncrypted.get() && writeExcel) { + collector.run(this::writeWorkbook); } - throwable = runStep(throwable, this::disposeSxssfWorkbook); - throwable = runStep(throwable, this::closeOutputStream); - if (writeExcel && !isOutputStreamEncrypt) { - throwable = runStep(throwable, encryption::doFileEncrypt07); + collector.run(this::disposeSxssfWorkbook); + collector.run(this::closeOutputStream); + if (writeExcel && !outputStreamEncrypted.get()) { + collector.run(encryption::doFileEncrypt07); } - throwable = runStep(throwable, this::closeTemplateInputStream); + collector.run(this::closeTemplateInputStream); encryption.clearEncrypt03(); removeThreadLocalCache(); - if (throwable != null) { - throw new ExcelGenerateException("Can not close IO.", throwable); - } + collector.throwIfPresent(t -> new ExcelGenerateException("Can not close IO.", t)); if (log.isDebugEnabled()) { log.debug("Finished write."); } @@ -573,10 +570,8 @@ private void writeAndCloseWorkbook(boolean onException) { /** * Writes the unencrypted workbook to the output stream if requested, then closes the workbook. */ - private void writeWorkbook(boolean writeExcel) throws IOException { - if (writeExcel) { - writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); - } + private void writeWorkbook() throws IOException { + writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); writeWorkbookHolder.getWorkbook().close(); } @@ -608,37 +603,6 @@ private void closeTemplateInputStream() throws IOException { } } - @FunctionalInterface - private interface FinishStep { - void run() throws Exception; - } - - /** - * Runs one step of the finishing sequence. A failure is recorded instead of stopping the sequence, and only the - * first failure is kept. - */ - private static Throwable runStep(Throwable throwable, FinishStep step) { - try { - step.run(); - } catch (Throwable t) { - return recordFailure(throwable, t); - } - return throwable; - } - - /** - * Returns the first recorded failure, attaching any later one to it as a suppressed exception. - */ - private static Throwable recordFailure(Throwable recorded, Throwable t) { - if (recorded == null) { - return t; - } - if (t != null && recorded != t) { - recorded.addSuppressed(t); - } - return recorded; - } - /** * Removes thread-local caches used during the write process. */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java new file mode 100644 index 000000000..7c8fcf6fe --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java @@ -0,0 +1,80 @@ +/* + * 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.util; + +import java.util.function.Function; + +/** + * Collects teardown failures while preserving the first error and attaching later failures as suppressed exceptions. + */ +public final class ExceptionCollector { + + private Throwable failure; + + /** + * Runs a step and records any failure without interrupting the remaining teardown sequence. + */ + public void run(ThrowableRunnable runnable) { + try { + runnable.run(); + } catch (Throwable t) { + failure = recordFailure(failure, t); + } + } + + /** + * Returns true if at least one recorded failure exists. + */ + public boolean hasFailure() { + return failure != null; + } + + /** + * Throws the collected failure transformed into the target exception type. + */ + public void throwIfPresent(Function transformer) throws E { + if (failure == null) { + return; + } + E transformed = transformer.apply(failure); + if (transformed != null) { + throw transformed; + } + throw new IllegalStateException("transformer must not return null", failure); + } + + /** + * Returns the first recorded failure while attaching any later errors as suppressed exceptions. + */ + private static Throwable recordFailure(Throwable recorded, Throwable t) { + if (recorded == null) { + return t; + } + if (t != null && recorded != t) { + recorded.addSuppressed(t); + } + return recorded; + } + + @FunctionalInterface + public interface ThrowableRunnable { + void run() throws Throwable; + } +} From 044fad07fe9b18dd69dbad3901911539baf9c163 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 11 Oct 2026 21:21:10 +0300 Subject: [PATCH 12/17] refactor: target-based encryption handling --- .../sheet/context/WorkbookEncryption.java | 40 ++++++++++++------- .../fesod/sheet/context/WriteContextImpl.java | 21 +++------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java index 9e358625e..c155e3a9d 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -48,10 +48,24 @@ */ final class WorkbookEncryption { + private enum Target { + NONE, + OUTPUT_STREAM, + FILE + } + private final WriteWorkbookHolder writeWorkbookHolder; + private final Target target; WorkbookEncryption(WriteWorkbookHolder writeWorkbookHolder) { this.writeWorkbookHolder = writeWorkbookHolder; + if (!isEncrypted(ExcelTypeEnum.XLSX)) { + target = Target.NONE; + } else if (writeWorkbookHolder.getFile() == null) { + target = Target.OUTPUT_STREAM; + } else { + target = Target.FILE; + } } /** @@ -72,18 +86,22 @@ private boolean isEncrypted(ExcelTypeEnum excelType) { && excelType.equals(writeWorkbookHolder.getExcelType()); } + boolean encryptsOutputStream() { + return target == Target.OUTPUT_STREAM; + } + + boolean encryptsFile() { + return target == Target.FILE; + } + /** * Encrypts the output stream for newer Excel formats. * - * @return True if encryption is successful, otherwise false. * @throws Exception If an error occurs during encryption. */ - boolean doOutputStreamEncrypt07() throws Exception { - if (!isEncrypted(ExcelTypeEnum.XLSX)) { - return false; - } - if (writeWorkbookHolder.getFile() != null) { - return false; + void doOutputStreamEncrypt07() throws Exception { + if (!encryptsOutputStream()) { + return; } File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); try { @@ -102,19 +120,13 @@ boolean doOutputStreamEncrypt07() throws Exception { throw new ExcelGenerateException("Can not delete temp File!"); } } - return true; } /** * To encrypt */ void doFileEncrypt07() throws Exception { - // Check if the password is empty or the file type is not xlsx, if so, return directly - if (!isEncrypted(ExcelTypeEnum.XLSX)) { - return; - } - // Check if the file is null, if so, return directly - if (writeWorkbookHolder.getFile() == null) { + if (!encryptsFile()) { return; } try { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 3f306accc..d372b9aa1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; @@ -537,25 +536,17 @@ private void afterSheetsDispose() { private void writeAndCloseWorkbook(boolean onException) { WorkbookEncryption encryption = new WorkbookEncryption(writeWorkbookHolder); ExceptionCollector collector = new ExceptionCollector(); - AtomicBoolean outputStreamEncrypted = new AtomicBoolean(false); - // Determine if you need to write excel - boolean writeExcel = !onException; - if (writeWorkbookHolder.getWriteExcelOnException()) { - writeExcel = Boolean.TRUE; - } - // No data is written if an exception is thrown + boolean writeExcel = !onException || writeWorkbookHolder.getWriteExcelOnException(); if (writeExcel) { - collector.run(() -> outputStreamEncrypted.set(encryption.doOutputStreamEncrypt07())); - if (collector.hasFailure()) { - writeExcel = false; + if (encryption.encryptsOutputStream()) { + collector.run(encryption::doOutputStreamEncrypt07); + } else { + collector.run(this::writeWorkbook); } } - if (!outputStreamEncrypted.get() && writeExcel) { - collector.run(this::writeWorkbook); - } collector.run(this::disposeSxssfWorkbook); collector.run(this::closeOutputStream); - if (writeExcel && !outputStreamEncrypted.get()) { + if (writeExcel && encryption.encryptsFile()) { collector.run(encryption::doFileEncrypt07); } collector.run(this::closeTemplateInputStream); From e2da114f9f85c1fbf4f0f605c76d16ea57ed0375 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 11 Oct 2026 22:14:47 +0300 Subject: [PATCH 13/17] chore: remove unnecessary early returns --- .../org/apache/fesod/sheet/context/WorkbookEncryption.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java index c155e3a9d..d012d441c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -100,9 +100,6 @@ boolean encryptsFile() { * @throws Exception If an error occurs during encryption. */ void doOutputStreamEncrypt07() throws Exception { - if (!encryptsOutputStream()) { - return; - } File tempXlsx = FileUtils.createTmpFile(UUID.randomUUID() + ".xlsx"); try { try (FileOutputStream tempFileOutputStream = new FileOutputStream(tempXlsx)) { @@ -126,9 +123,6 @@ void doOutputStreamEncrypt07() throws Exception { * To encrypt */ void doFileEncrypt07() throws Exception { - if (!encryptsFile()) { - return; - } try { // Use try-with-resources to automatically close resources, encrypt and write the file try (POIFSFileSystem fileSystem = openFileSystemAndEncrypt(writeWorkbookHolder.getFile()); From fa7569fc03324de3edd2ad162946ba57718af92d Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Mon, 12 Oct 2026 18:24:11 +0300 Subject: [PATCH 14/17] chore: clean up license --- .../org/apache/fesod/sheet/context/WorkbookEncryption.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java index d012d441c..147b16a53 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -17,12 +17,6 @@ * under the License. */ -/* - * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. - * - * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. - */ - package org.apache.fesod.sheet.context; import java.io.File; From d13a1d5003aacfbb36e5b8f7a4f86a25fc353849 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Mon, 12 Oct 2026 18:42:24 +0300 Subject: [PATCH 15/17] chore: rename encryption check method --- .../org/apache/fesod/sheet/context/WorkbookEncryption.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java index 147b16a53..1feadb3cb 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WorkbookEncryption.java @@ -53,7 +53,7 @@ private enum Target { WorkbookEncryption(WriteWorkbookHolder writeWorkbookHolder) { this.writeWorkbookHolder = writeWorkbookHolder; - if (!isEncrypted(ExcelTypeEnum.XLSX)) { + if (!hasPasswordFor(ExcelTypeEnum.XLSX)) { target = Target.NONE; } else if (writeWorkbookHolder.getFile() == null) { target = Target.OUTPUT_STREAM; @@ -66,7 +66,7 @@ private enum Target { * Clears encryption settings for older Excel formats. */ void clearEncrypt03() { - if (!isEncrypted(ExcelTypeEnum.XLS)) { + if (!hasPasswordFor(ExcelTypeEnum.XLS)) { return; } Biff8EncryptionKey.setCurrentUserPassword(null); @@ -75,7 +75,7 @@ void clearEncrypt03() { /** * Checks whether the workbook is password-protected and written as the given type. */ - private boolean isEncrypted(ExcelTypeEnum excelType) { + private boolean hasPasswordFor(ExcelTypeEnum excelType) { return !StringUtils.isEmpty(writeWorkbookHolder.getPassword()) && excelType.equals(writeWorkbookHolder.getExcelType()); } From d7947c0b9154d41c1e6f3e9cf84f3a70b784a625 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Mon, 12 Oct 2026 18:46:31 +0300 Subject: [PATCH 16/17] chore: simplify null check --- .../java/org/apache/fesod/sheet/util/ExceptionCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java index 7c8fcf6fe..b7d4e947f 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java @@ -67,7 +67,7 @@ private static Throwable recordFailure(Throwable recorded, Throwable t) { if (recorded == null) { return t; } - if (t != null && recorded != t) { + if (recorded != t) { recorded.addSuppressed(t); } return recorded; From 59d32df139367cbb5a55a9716953371155848505 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Mon, 12 Oct 2026 18:51:50 +0300 Subject: [PATCH 17/17] docs: improve ExceptionCollector documentation --- .../apache/fesod/sheet/util/ExceptionCollector.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java index b7d4e947f..ac4b21fe2 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExceptionCollector.java @@ -22,14 +22,14 @@ import java.util.function.Function; /** - * Collects teardown failures while preserving the first error and attaching later failures as suppressed exceptions. + * Collects failures from multiple actions, retaining the first and adding subsequent failures as suppressed exceptions. */ public final class ExceptionCollector { private Throwable failure; /** - * Runs a step and records any failure without interrupting the remaining teardown sequence. + * Runs the action and records any thrown failure, allowing subsequent actions to run. */ public void run(ThrowableRunnable runnable) { try { @@ -39,9 +39,6 @@ public void run(ThrowableRunnable runnable) { } } - /** - * Returns true if at least one recorded failure exists. - */ public boolean hasFailure() { return failure != null; } @@ -60,9 +57,6 @@ public void throwIfPresent(Function transfor throw new IllegalStateException("transformer must not return null", failure); } - /** - * Returns the first recorded failure while attaching any later errors as suppressed exceptions. - */ private static Throwable recordFailure(Throwable recorded, Throwable t) { if (recorded == null) { return t;