diff --git a/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/ZipPackage.java b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/ZipPackage.java index 9f1bd5764aa..35e20e855ff 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/ZipPackage.java +++ b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/ZipPackage.java @@ -27,7 +27,6 @@ Licensed to the Apache Software Foundation (ASF) under one or more import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -52,7 +51,6 @@ Licensed to the Apache Software Foundation (ASF) under one or more import org.apache.poi.openxml4j.util.ZipFileZipEntrySource; import org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource; import org.apache.poi.util.IOUtils; -import org.apache.poi.util.TempFile; /** * Physical zip package. @@ -62,6 +60,7 @@ public final class ZipPackage extends OPCPackage { private static final String SETTINGS_XML = "settings.xml"; private static boolean useTempFilePackageParts = false; private static boolean encryptTempFilePackageParts = false; + private static boolean bufferTempFilePackageParts = false; private static final Logger LOG = LogManager.getLogger(ZipPackage.class); @@ -85,6 +84,13 @@ public static void setEncryptTempFilePackageParts(boolean encryptTempFiles) { encryptTempFilePackageParts = encryptTempFiles; } + /** + * @param bufferTempFiles whether to buffer package part temp files + */ + public static void setBufferTempFilePackageParts(boolean bufferTempFiles) { + bufferTempFilePackageParts = bufferTempFiles; + } + /** * @return whether package part data is stored in temp files to save memory */ @@ -99,6 +105,13 @@ public static boolean encryptTempFilePackageParts() { return encryptTempFilePackageParts; } + /** + * @return whether package part temp files are buffered + */ + public static boolean bufferTempFilePackageParts() { + return bufferTempFilePackageParts; + } + /** * Constructor. Creates a new, empty ZipPackage. */ @@ -422,8 +435,16 @@ protected PackagePart createPartImpl(PackagePartName partName, try { if (useTempFilePackageParts) { if (encryptTempFilePackageParts) { - return new EncryptedTempFilePackagePart(this, partName, contentType, loadRelationships); - } else { + if (bufferTempFilePackageParts) { + return new BufferedEncryptedTempFilePackagePart(this, partName, contentType, loadRelationships); + } else { + return new EncryptedTempFilePackagePart(this, partName, contentType, loadRelationships); + } + } + else if (bufferTempFilePackageParts) { + return new BufferedTempFilePackagePart(this, partName, contentType, loadRelationships); + } + else { return new TempFilePackagePart(this, partName, contentType, loadRelationships); } } else { diff --git a/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedEncryptedTempFilePackagePart.java b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedEncryptedTempFilePackagePart.java new file mode 100644 index 00000000000..91738a10f7f --- /dev/null +++ b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedEncryptedTempFilePackagePart.java @@ -0,0 +1,147 @@ +/* ==================================================================== + 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.poi.openxml4j.opc.internal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.exceptions.OpenXML4JException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.internal.marshallers.ZipPartMarshaller; +import org.apache.poi.poifs.crypt.temp.EncryptedTempData; +import org.apache.poi.util.Beta; +import org.apache.poi.util.IOUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * (Experimental) Buffered Encrypted Temp File version of a package part. + * + * @since POI 5.2.4 + */ +@Beta +public final class BufferedEncryptedTempFilePackagePart extends PackagePart { + private static int fileBufferSize = 1024 * 1024; // 1 MB + private static final Logger LOG = LogManager.getLogger(BufferedEncryptedTempFilePackagePart.class); + + /** + * Storage for the part data. + */ + private EncryptedTempData tempFile; + + /** + * Constructor. + * + * @param pack + * The owner package. + * @param partName + * The part name. + * @param contentType + * The content type. + * @throws InvalidFormatException + * If the specified URI is not OPC compliant. + * @throws IOException + * If temp file cannot be created. + */ + public BufferedEncryptedTempFilePackagePart(OPCPackage pack, PackagePartName partName, + String contentType) throws InvalidFormatException, IOException { + this(pack, partName, contentType, true); + } + + /** + * Constructor. + * + * @param pack + * The owner package. + * @param partName + * The part name. + * @param contentType + * The content type. + * @param loadRelationships + * Specify if the relationships will be loaded. + * @throws InvalidFormatException + * If the specified URI is not OPC compliant. + * @throws IOException + * If temp file cannot be created. + */ + public BufferedEncryptedTempFilePackagePart(OPCPackage pack, PackagePartName partName, + String contentType, boolean loadRelationships) + throws InvalidFormatException, IOException { + super(pack, partName, new ContentType(contentType), loadRelationships); + tempFile = new EncryptedTempData(); + } + + @Override + protected InputStream getInputStreamImpl() throws IOException { + return new BufferedInputStream(tempFile.getInputStream(), fileBufferSize); + } + + @Override + protected OutputStream getOutputStreamImpl() throws IOException { + return new BufferedOutputStream(tempFile.getOutputStream(), fileBufferSize); + } + + @Override + public long getSize() { + return tempFile.getByteCount(); + } + + @Override + public void clear() { + try(OutputStream os = getOutputStreamImpl()) { + os.write(new byte[0]); + } catch (IOException e) { + LOG.atWarn().log("Failed to clear data in temp file", e); + } + } + + @Override + public boolean save(OutputStream os) throws OpenXML4JException { + return new ZipPartMarshaller().marshall(this, os); + } + + @Override + public boolean load(InputStream is) throws InvalidFormatException { + try (OutputStream os = getOutputStreamImpl()) { + IOUtils.copy(is, os); + } catch(IOException e) { + throw new InvalidFormatException(e.getMessage(), e); + } + + // All done + return true; + } + + @Override + public void close() { + tempFile.dispose(); + } + + @Override + public void flush() { + // Do nothing + } +} diff --git a/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedTempFilePackagePart.java b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedTempFilePackagePart.java new file mode 100644 index 00000000000..16172bd01c9 --- /dev/null +++ b/poi-ooxml/src/main/java/org/apache/poi/openxml4j/opc/internal/BufferedTempFilePackagePart.java @@ -0,0 +1,159 @@ +/* ==================================================================== + 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.poi.openxml4j.opc.internal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.exceptions.OpenXML4JException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.internal.marshallers.ZipPartMarshaller; +import org.apache.poi.util.Beta; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.TempFile; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * (Experimental) Buffered Temp File version of a package part. + * + * @since POI 5.4.2 + */ +@Beta +public final class BufferedTempFilePackagePart extends PackagePart { + private static int fileBufferSize = 1024 * 1024; // 1 MB + private static final Logger LOG = LogManager.getLogger(BufferedTempFilePackagePart.class); + + /** + * Storage for the part data. + */ + private File tempFile; + + /** + * Constructor. + * + * @param pack + * The owner package. + * @param partName + * The part name. + * @param contentType + * The content type. + * @throws InvalidFormatException + * If the specified URI is not OPC compliant. + * @throws IOException + * If temp file cannot be created. + */ + public BufferedTempFilePackagePart(OPCPackage pack, PackagePartName partName, + String contentType) throws InvalidFormatException, IOException { + this(pack, partName, contentType, true); + } + + /** + * Constructor. + * + * @param pack + * The owner package. + * @param partName + * The part name. + * @param contentType + * The content type. + * @param loadRelationships + * Specify if the relationships will be loaded. + * @throws InvalidFormatException + * If the specified URI is not OPC compliant. + * @throws IOException + * If temp file cannot be created. + */ + public BufferedTempFilePackagePart(OPCPackage pack, PackagePartName partName, + String contentType, boolean loadRelationships) + throws InvalidFormatException, IOException { + super(pack, partName, new ContentType(contentType), loadRelationships); + tempFile = TempFile.createTempFile("poi-package-part", ".tmp"); + } + + /** + * + * @param bufferSize + * Size by whic + */ + public static void setBufferSize(int bufferSize) { + fileBufferSize = bufferSize; + } + + @Override + protected InputStream getInputStreamImpl() throws IOException { + return new BufferedInputStream(new FileInputStream(tempFile), fileBufferSize); + } + + @Override + protected OutputStream getOutputStreamImpl() throws IOException { + return new BufferedOutputStream(new FileOutputStream(tempFile), fileBufferSize); + } + + @Override + public long getSize() { + return tempFile.length(); + } + + @Override + public void clear() { + try(OutputStream os = getOutputStreamImpl()) { + os.write(new byte[0]); + } catch (IOException e) { + LOG.atWarn().log("Failed to clear data in temp file", e); + } + } + + @Override + public boolean save(OutputStream os) throws OpenXML4JException { + return new ZipPartMarshaller().marshall(this, os); + } + + @Override + public boolean load(InputStream is) throws InvalidFormatException { + try (OutputStream os = getOutputStreamImpl()) { + IOUtils.copy(is, os); + } catch(IOException e) { + throw new InvalidFormatException(e.getMessage(), e); + } + + // All done + return true; + } + + @Override + public void close() { + if (!tempFile.delete()) { + LOG.atInfo().log("Failed to delete temp file; may already have been closed and deleted"); + } + } + + @Override + public void flush() { + // Do nothing + } +} diff --git a/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedEncryptedTempFilePackagePart.java b/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedEncryptedTempFilePackagePart.java new file mode 100644 index 00000000000..fe87ace4aff --- /dev/null +++ b/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedEncryptedTempFilePackagePart.java @@ -0,0 +1,52 @@ +/* ==================================================================== + 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.poi.openxml4j.opc; + +import org.apache.poi.openxml4j.OpenXML4JTestDataSamples; +import org.apache.poi.openxml4j.opc.internal.BufferedEncryptedTempFilePackagePart; +import org.apache.poi.openxml4j.opc.internal.EncryptedTempFilePackagePart; +import org.apache.poi.util.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestBufferedEncryptedTempFilePackagePart { + @Test + void testRoundTrip() throws Exception { + String text = UUID.randomUUID().toString(); + byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + String filepath = OpenXML4JTestDataSamples.getSampleFileName("sample.docx"); + + try (OPCPackage p = OPCPackage.open(filepath, PackageAccess.READ)) { + PackagePartName name = new PackagePartName("/test.txt", true); + BufferedEncryptedTempFilePackagePart part = new BufferedEncryptedTempFilePackagePart(p, name, "text/plain"); + try (OutputStream os = part.getOutputStream()) { + os.write(bytes); + } + assertEquals(bytes.length, part.getSize()); + try (InputStream is = part.getInputStream()) { + assertEquals(text, new String(IOUtils.toByteArray(is), StandardCharsets.UTF_8)); + } + } + } +} diff --git a/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedTempFilePackagePart.java b/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedTempFilePackagePart.java new file mode 100644 index 00000000000..397c0f2f6f3 --- /dev/null +++ b/poi-ooxml/src/test/java/org/apache/poi/openxml4j/opc/TestBufferedTempFilePackagePart.java @@ -0,0 +1,35 @@ +package org.apache.poi.openxml4j.opc; + +import org.apache.poi.openxml4j.OpenXML4JTestDataSamples; +import org.apache.poi.openxml4j.opc.internal.BufferedTempFilePackagePart; +import org.apache.poi.openxml4j.opc.internal.TempFilePackagePart; +import org.apache.poi.util.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestBufferedTempFilePackagePart { + @Test + void testRoundTrip() throws Exception { + String text = UUID.randomUUID().toString(); + byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + String filepath = OpenXML4JTestDataSamples.getSampleFileName("sample.docx"); + + try (OPCPackage p = OPCPackage.open(filepath, PackageAccess.READ)) { + PackagePartName name = new PackagePartName("/test.txt", true); + BufferedTempFilePackagePart part = new BufferedTempFilePackagePart(p, name, "text/plain"); + try (OutputStream os = part.getOutputStream()) { + os.write(bytes); + } + assertEquals(bytes.length, part.getSize()); + try (InputStream is = part.getInputStream()) { + assertEquals(text, new String(IOUtils.toByteArray(is), StandardCharsets.UTF_8)); + } + } + } +} diff --git a/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java b/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java index ac903cc661c..df34290caaa 100644 --- a/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java +++ b/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFWorkbook.java @@ -1243,9 +1243,11 @@ void testNewWorkbookWithTempFilePackageParts() throws Exception { try(UnsynchronizedByteArrayOutputStream bos = new UnsynchronizedByteArrayOutputStream()) { assertFalse(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts defaults to false?"); assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts defaults to false?"); ZipPackage.setUseTempFilePackageParts(true); assertTrue(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts was modified?"); assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts was not modified?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts was not modified?"); try (XSSFWorkbook workbook = new XSSFWorkbook()) { XSSFSheet sheet = workbook.createSheet("sheet1"); XSSFRow row = sheet.createRow(0); @@ -1267,10 +1269,12 @@ void testNewWorkbookWithEncryptedTempFilePackageParts() throws Exception { try(UnsynchronizedByteArrayOutputStream bos = new UnsynchronizedByteArrayOutputStream()) { assertFalse(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts defaults to false?"); assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts defaults to false?"); ZipPackage.setUseTempFilePackageParts(true); ZipPackage.setEncryptTempFilePackageParts(true); assertTrue(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts was modified?"); assertTrue(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts was modified?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts was not modified?"); try (XSSFWorkbook workbook = new XSSFWorkbook()) { XSSFSheet sheet = workbook.createSheet("sheet1"); XSSFRow row = sheet.createRow(0); @@ -1288,6 +1292,64 @@ void testNewWorkbookWithEncryptedTempFilePackageParts() throws Exception { } } + @Test + void testNewWorkbookWithBufferedEncryptedTempFilePackageParts() throws Exception { + try(UnsynchronizedByteArrayOutputStream bos = new UnsynchronizedByteArrayOutputStream()) { + assertFalse(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts defaults to false?"); + ZipPackage.setUseTempFilePackageParts(true); + ZipPackage.setEncryptTempFilePackageParts(true); + ZipPackage.setBufferTempFilePackageParts(true); + assertTrue(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts was modified?"); + assertTrue(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts was modified?"); + assertTrue(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts was modified?"); + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("sheet1"); + XSSFRow row = sheet.createRow(0); + XSSFCell cell0 = row.createCell(0); + cell0.setCellValue(""); + XSSFCell cell1 = row.createCell(1); + cell1.setCellErrorValue(FormulaError.DIV0); + XSSFCell cell2 = row.createCell(2); + cell2.setCellErrorValue(FormulaError.FUNCTION_NOT_IMPLEMENTED); + workbook.write(bos); + } finally { + ZipPackage.setUseTempFilePackageParts(false); + ZipPackage.setEncryptTempFilePackageParts(false); + ZipPackage.setBufferTempFilePackageParts(false); + } + } + } + + @Test + void testNewWorkbookWithBufferedTempFilePackageParts() throws Exception { + try(UnsynchronizedByteArrayOutputStream bos = new UnsynchronizedByteArrayOutputStream()) { + assertFalse(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts defaults to false?"); + assertFalse(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts defaults to false?"); + ZipPackage.setUseTempFilePackageParts(true); + ZipPackage.setBufferTempFilePackageParts(true); + assertTrue(ZipPackage.useTempFilePackageParts(), "useTempFilePackageParts was modified?"); + assertFalse(ZipPackage.encryptTempFilePackageParts(), "encryptTempFilePackageParts defaults to false?"); + assertTrue(ZipPackage.bufferTempFilePackageParts(), "bufferTempFilePackageParts was modified?"); + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("sheet1"); + XSSFRow row = sheet.createRow(0); + XSSFCell cell0 = row.createCell(0); + cell0.setCellValue(""); + XSSFCell cell1 = row.createCell(1); + cell1.setCellErrorValue(FormulaError.DIV0); + XSSFCell cell2 = row.createCell(2); + cell2.setCellErrorValue(FormulaError.FUNCTION_NOT_IMPLEMENTED); + workbook.write(bos); + } finally { + ZipPackage.setUseTempFilePackageParts(false); + ZipPackage.setBufferTempFilePackageParts(false); + } + } + } + @Test void testLinkExternalWorkbook() throws Exception { String nameA = "link-external-workbook-a.xlsx";