From 6447b1d90b87ea273acbac7ecc189ed005efe0e7 Mon Sep 17 00:00:00 2001 From: fillg1 Date: Thu, 9 Apr 2026 06:31:16 +0200 Subject: [PATCH] Initial commit --- .idea/.gitignore | 10 + .idea/.name | 1 + .idea/go.imports.xml | 10 + .idea/gradle.xml | 17 + .idea/misc.xml | 10 + .idea/vcs.xml | 6 + build.gradle | 9 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 0 settings.gradle | 2 +- src/main/java/ch/illgner/OrderCalculator.java | 19 -- .../ch/createzip/PostBoxZipFileWriter.java | 138 ++++++++ .../java/illgner/ch/createzip/ZipResult.java | 23 ++ .../ch/encryption/IInputStreamEnhancer.java | 15 + .../ch/encryption/IOutputStreamEnhancer.java | 18 ++ .../java/ch/illgner/OrderCalculatorTest.java | 32 -- .../service/PostBoxZipFileWriterTest.java | 306 ++++++++++++++++++ 19 files changed, 565 insertions(+), 54 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/go.imports.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml mode change 100644 => 100755 build.gradle create mode 100755 gradle.properties mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.jar mode change 100644 => 100755 gradle/wrapper/gradle-wrapper.properties mode change 100644 => 100755 gradlew.bat mode change 100644 => 100755 settings.gradle delete mode 100644 src/main/java/ch/illgner/OrderCalculator.java create mode 100755 src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java create mode 100755 src/main/java/illgner/ch/createzip/ZipResult.java create mode 100755 src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java create mode 100755 src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java delete mode 100644 src/test/java/ch/illgner/OrderCalculatorTest.java create mode 100755 src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b26bca2 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +benchmark \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b37fcda --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7dcc043 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle old mode 100644 new mode 100755 index 304e7e2..df545ab --- a/build.gradle +++ b/build.gradle @@ -10,11 +10,18 @@ repositories { } dependencies { + implementation group: 'commons-io', name: 'commons-io', version: '2.21.0' + implementation "org.apache.commons:commons-lang3:3.20.0" + implementation "org.slf4j:jul-to-slf4j:$sl4j_version" + implementation "org.slf4j:log4j-over-slf4j:$sl4j_version" + testImplementation platform('org.junit:junit-bom:6.0.0') testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '3.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..28f4b38 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +sl4j_version=1.7.35 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100644 new mode 100755 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100644 new mode 100755 index 43d9bce..be2bdc2 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Wed Apr 08 21:42:31 CEST 2026 +#Wed Apr 08 22:08:17 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle old mode 100644 new mode 100755 index 1a25252..e3abb74 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'Benchmark' \ No newline at end of file +rootProject.name = 'benchmark' \ No newline at end of file diff --git a/src/main/java/ch/illgner/OrderCalculator.java b/src/main/java/ch/illgner/OrderCalculator.java deleted file mode 100644 index 091ac7a..0000000 --- a/src/main/java/ch/illgner/OrderCalculator.java +++ /dev/null @@ -1,19 +0,0 @@ -package ch.illgner; - -public class OrderCalculator { - - /** - * Berechnet den Endpreis nach Rabatt und inklusive 19% MwSt. - * - * @param basePrice Der Nettopreis - * @param discountRate Der Rabatt (z.B. 0.1 für 10%) - * @return Der Endpreis gerundet auf 2 Nachkommastellen - */ - public double calculateFinalPrice(double basePrice, double discountRate) { - double discountedPrice = basePrice * discountRate; - - double priceWithTax = discountedPrice * 1.19; - - return priceWithTax; - } -} \ No newline at end of file diff --git a/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java new file mode 100755 index 0000000..ef46a0a --- /dev/null +++ b/src/main/java/illgner/ch/createzip/PostBoxZipFileWriter.java @@ -0,0 +1,138 @@ +package illgner.ch.createzip; + +import illgner.ch.encryption.IOutputStreamEnhancer; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.CountingOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.NoSuchPaddingException; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.EnumSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class PostBoxZipFileWriter implements Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(PostBoxZipFileWriter.class); + + private static final Set ZIP_FILE_PERMISSIONS = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ); + + private final Path destinationDir; + private final String baseName; + private final int chunkSize; + private final int bufferSize; + private final ZipResult zipResult; + IOutputStreamEnhancer outputStreamEnhancer; + + private ZipOutputStream zipOutputStream; + + private int fileCount = 1; + private long bytesWritten = 0; + private File actualZipFile; + private CountingOutputStream countingOutputStream; + + public PostBoxZipFileWriter(Path destinationDir, String baseName, int chunkSize, int bufferSize, IOutputStreamEnhancer outputStreamEnhancer) { + this.destinationDir = destinationDir; + this.baseName = baseName; + this.chunkSize = chunkSize; + this.bufferSize = bufferSize; + this.zipResult = new ZipResult(); + this.outputStreamEnhancer = outputStreamEnhancer; + } + + private ZipOutputStream createNewZipOutputStream() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + String actualZipFilename = createOutputFilename(); + actualZipFile = destinationDir.resolve(actualZipFilename).toFile(); + + LOG.info("Creating zip part {}: {}", fileCount, actualZipFilename); + + countingOutputStream = new CountingOutputStream(outputStreamEnhancer.enhance(new BufferedOutputStream(new FileOutputStream(actualZipFile), bufferSize))); + return new ZipOutputStream(countingOutputStream); + } + + private String createOutputFilename() { + return String.format("%s_%02d.zip", baseName, fileCount); + } + + public void write(InputStream inputStream, String path) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (countingOutputStream == null) { + zipOutputStream = createNewZipOutputStream(); + } + if (bytesWritten >= chunkSize) { + switchOutputFile(); + } + LOG.info("Writing file {} to zip", path); + zipOutputStream.putNextEntry(new ZipEntry(path)); + bytesWritten += IOUtils.copyLarge(inputStream, zipOutputStream); + zipOutputStream.closeEntry(); + } + + private void switchOutputFile() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + LOG.info("Switching zip"); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + fileCount++; + zipOutputStream = createNewZipOutputStream(); + bytesWritten = 0; + } + + public void close() throws IOException { + LOG.info("Closing zip {}", actualZipFile.getName()); + zipOutputStream.flush(); + zipOutputStream.close(); + addZipToResult(); + } + + public void writeReadMe() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + + if (zipOutputStream != null) { + LOG.info("Skip adding Readme"); + return; + } + + try (InputStream readmeStream = this.getClass().getClassLoader().getResourceAsStream("Readme.txt")) { + if (readmeStream == null) { + throw new IOException("Cannot open Readme"); + } + + LOG.info("Writing Readme"); + write(readmeStream, "Readme.txt"); + } catch (IOException e) { + throw new IOException("Could not write Readme", e); + } + } + + public ZipResult getZipResult() { + return this.zipResult; + } + + private void addZipToResult() throws IOException { + zipResult.add(actualZipFile.getName(), countingOutputStream.getCount()); + Files.setPosixFilePermissions(actualZipFile.toPath(), ZIP_FILE_PERMISSIONS); + } +} diff --git a/src/main/java/illgner/ch/createzip/ZipResult.java b/src/main/java/illgner/ch/createzip/ZipResult.java new file mode 100755 index 0000000..947bf33 --- /dev/null +++ b/src/main/java/illgner/ch/createzip/ZipResult.java @@ -0,0 +1,23 @@ +package illgner.ch.createzip; + +import java.util.ArrayList; +import java.util.List; + +public class ZipResult { + + private final List zipFilenames = new ArrayList<>(); + private final List zipFileLengths = new ArrayList<>(); + + void add(String fileName, long bytesWritten) { + zipFilenames.add(fileName); + zipFileLengths.add(bytesWritten); + } + + public List getFileLengths() { + return zipFileLengths; + } + + public List getFilenames() { + return zipFilenames; + } +} diff --git a/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java new file mode 100755 index 0000000..94dd475 --- /dev/null +++ b/src/main/java/illgner/ch/encryption/IInputStreamEnhancer.java @@ -0,0 +1,15 @@ +package illgner.ch.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IInputStreamEnhancer { + + InputStream enhance(InputStream inputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; + +} diff --git a/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java new file mode 100755 index 0000000..d17ad88 --- /dev/null +++ b/src/main/java/illgner/ch/encryption/IOutputStreamEnhancer.java @@ -0,0 +1,18 @@ +package illgner.ch.encryption; + +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public interface IOutputStreamEnhancer { + /** + * This method enhances the given input stream {@link OutputStream} wrapping with the additional functionality (in our case with encryption) + * @param outputStream the original stream {@link OutputStream} to be enhanced. + * @return A new {@link OutputStream} that wraps the functionality to input stream. + */ + OutputStream enhance(OutputStream outputStream) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidAlgorithmParameterException, NoSuchProviderException; +} diff --git a/src/test/java/ch/illgner/OrderCalculatorTest.java b/src/test/java/ch/illgner/OrderCalculatorTest.java deleted file mode 100644 index f62acda..0000000 --- a/src/test/java/ch/illgner/OrderCalculatorTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.illgner; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class OrderCalculatorTest { - - @Test - public void testCalculateFinalPrice_NormalScenario() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 100€ Basis, 10% Rabatt = 90€. - // 90€ + 19% MwSt (17.10€) = 107.10€ - double result = calculator.calculateFinalPrice(100.0, 0.10); - - assertEquals(107.10, result, "Der Endpreis für 100€ mit 10% Rabatt muss 107.10 sein."); - } - - @Test - public void testCalculateFinalPrice_Rounding() { - OrderCalculator calculator = new OrderCalculator(); - - // Szenario: 10.55€ Basis, 5% Rabatt = 10.0225€ - // 10.0225€ + 19% MwSt = 11.926775€ -> Gerundet: 11.93€ - double result = calculator.calculateFinalPrice(10.55, 0.05); - - assertEquals(11.93, result, "Der Preis muss korrekt auf 2 Stellen gerundet werden."); - } -} \ No newline at end of file diff --git a/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java new file mode 100755 index 0000000..837ddd2 --- /dev/null +++ b/src/test/java/illgner/ch/jobs/postboxbulk/service/PostBoxZipFileWriterTest.java @@ -0,0 +1,306 @@ +package illgner.ch.jobs.postboxbulk.service; + +import illgner.ch.createzip.PostBoxZipFileWriter; +import illgner.ch.createzip.ZipResult; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.io.FileMatchers.aFileNamed; +import static org.hamcrest.io.FileMatchers.anExistingFile; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PostBoxZipFileWriterTest { + + private static final Path DESTINATION_DIR = Paths.get("./destination"); + private static final int ONE_BYTE = 1024; + private static final int HALF_BYTE = ONE_BYTE / 2; + private static final int ZIP_BUFFER_SIZE_IN_BYTE = 2 * ONE_BYTE; + private static final int ZIP_CHUNK_SIZE_IN_BYTE = 3 * ONE_BYTE; + + @BeforeAll + static void setUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @AfterAll + static void cleanUp() throws IOException { + FileUtils.deleteDirectory(DESTINATION_DIR.toFile()); + boolean directoryCreated = DESTINATION_DIR.toFile().mkdir(); + assertTrue(directoryCreated); + } + + @Test + void singleFileSmallerThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileSmallerThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileName = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + File createdZipFile = DESTINATION_DIR.resolve(expectedZippedFileName).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileName))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZipFile.length()))); + assertThat(createdZipFile, is(anExistingFile())); + assertThat(createdZipFile, is(aFileNamed(equalTo(expectedZippedFileName)))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZipFile))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileName))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(fileSimpleContent))); + } + } + + @Test + void singleFileGreaterThanChunkSizeToOneZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] fileSimpleContent = RandomStringUtils.randomAlphanumeric(4 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + ZipResult zipResult; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileName = "singleFileGreaterThanChunkSizeToOneZip.txt"; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(fileSimpleContent), inputFileName); + } + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + zipResult = postBoxZipFileWriter.getZipResult(); + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileName); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(fileSimpleContent))); + } + + @Test + void multipleFileSmallerThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] firstFile = RandomStringUtils.randomAlphanumeric(ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = "uts öäü ÖÄÜ ß".getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFileSmallerThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFileSmallerThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } + + zipResult = postBoxZipFileWriter.getZipResult(); + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + File createdZip = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertThat(createdZip, is(anExistingFile())); + assertThat(createdZip, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(zipResult.getFilenames().get(0), is(equalTo(expectedZippedFileNamePartOne))); + assertThat(zipResult.getFileLengths().get(0), is(equalTo(createdZip.length()))); + + // check zip content + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(createdZip))) { + assertThat(Objects.requireNonNull(zipInputStream.getNextEntry()).getName(), is(equalTo(inputFileNameOne))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(firstFile))); + assertThat(zipInputStream.getNextEntry().getName(), is(equalTo(inputFileNameTwo))); + assertThat(IOUtils.toByteArray(zipInputStream), is(equalTo(secondFile))); + } + } + + @Test + void multipleFilesTogetherGreaterThanChunkSizeToSingleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + String text1 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + String text2 = RandomStringUtils.randomAlphanumeric(2 * ONE_BYTE + HALF_BYTE); + byte[] firstFile = text1.getBytes(StandardCharsets.UTF_8); + byte[] secondFile = text2.getBytes(StandardCharsets.UTF_8); + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + String inputFileNameOne = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip1.txt"; + String inputFileNameTwo = "multipleFilesTogetherGreaterThanChunkSizeToSingleZip2.txt"; + + ZipResult zipResult; + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, ZIP_CHUNK_SIZE_IN_BYTE, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), inputFileNameOne); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), inputFileNameTwo); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + + assertThat(zipResult.getFilenames(), hasSize(1)); + assertThat(zipResult.getFileLengths(), hasSize(1)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + + File createdZipOne = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + + assertThat(createdZipOne, is(anExistingFile())); + assertThat(createdZipOne, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + + // check zip content + extractSplitZipFiles(new File[]{createdZipOne}); + File extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameOne); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + inputFileNameTwo); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + } + + @Test + void multipleBigFilesToMultipleZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + ZipResult zipResult; + int bigChuckSize = ONE_BYTE * 50; + int bigBufferSize = ONE_BYTE * 10; + // First two files will be zipped together and third file will be zipped in the second file. + byte[] firstFile = RandomStringUtils.randomAlphanumeric(40 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] secondFile = RandomStringUtils.randomAlphanumeric(70 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + byte[] thirdFile = RandomStringUtils.randomAlphanumeric(10 * ONE_BYTE).getBytes(StandardCharsets.UTF_8); + String[] originalFileNames = {"multipleBigFilesToMultipleZip1.txt", "multipleBigFilesToMultipleZip2.txt", "multipleBigFilesToMultipleZip3.txt"}; + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, outputFileName, bigChuckSize, bigBufferSize, (OutputStream outputStream) -> outputStream); + try (postBoxZipFileWriter) { + postBoxZipFileWriter.write(new ByteArrayInputStream(firstFile), originalFileNames[0]); + postBoxZipFileWriter.write(new ByteArrayInputStream(secondFile), originalFileNames[1]); + postBoxZipFileWriter.write(new ByteArrayInputStream(thirdFile), originalFileNames[2]); + } finally { + zipResult = postBoxZipFileWriter.getZipResult(); + } + + String expectedZippedFileNamePartOne = outputFileName + "_01.zip"; + String expectedZippedFileNamePartTwo = outputFileName + "_02.zip"; + + assertThat(zipResult.getFilenames(), hasSize(2)); + assertThat(zipResult.getFileLengths(), hasSize(2)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartOne)); + assertTrue(zipResult.getFilenames().contains(expectedZippedFileNamePartTwo)); + + File part1 = DESTINATION_DIR.resolve(expectedZippedFileNamePartOne).toFile(); + File part2 = DESTINATION_DIR.resolve(expectedZippedFileNamePartTwo).toFile(); + + File[] zippedFiles = {part1, part2}; + + assertThat(part1, is(anExistingFile())); + assertThat(part1, is(aFileNamed(equalTo(expectedZippedFileNamePartOne)))); + assertThat(part2, is(anExistingFile())); + assertThat(part2, is(aFileNamed(equalTo(expectedZippedFileNamePartTwo)))); + + extractSplitZipFiles(zippedFiles); + + // check zip content + File extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[0]); + byte[] extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(firstFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[1]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(secondFile))); + + extractedFile = new File(DESTINATION_DIR + "/" + originalFileNames[2]); + extractedFileBytes = IOUtils.toByteArray(new FileInputStream(extractedFile)); + assertThat(extractedFileBytes, is(equalTo(thirdFile))); + } + + @Test + void createBigZip() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + // first implementation crashed with an OutOfMemoryException while copying an 50MB Stream, + // so we added a test for this + + try (PostBoxZipFileWriter zipFileWriter = new PostBoxZipFileWriter( + DESTINATION_DIR, "testBigMulti", 100 * 1000 * 1000, ZIP_BUFFER_SIZE_IN_BYTE, (OutputStream outputStream) -> outputStream)) { + for (int i = 0; i < 10; i++) { + byte[] content = RandomStringUtils.randomAlphanumeric(50 * 1000 * 1000).getBytes(); + zipFileWriter.write(new ByteArrayInputStream(content), "test" + i); + } + } + } + + @Test + void addCompressibleFiles() throws InvalidAlgorithmParameterException, NoSuchPaddingException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException { + byte[] content1 = new byte[10000]; + byte[] content2 = new byte[10000]; + byte[] content3 = new byte[10000]; + + String outputFileName = RandomStringUtils.randomAlphanumeric(10); + + ZipResult zipResult; + try (PostBoxZipFileWriter postBoxZipFileWriter = new PostBoxZipFileWriter(DESTINATION_DIR, outputFileName, 1000, 2048, (OutputStream outputStream) -> outputStream)) { + postBoxZipFileWriter.write(new ByteArrayInputStream(content1), "file1"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content2), "file2"); + postBoxZipFileWriter.write(new ByteArrayInputStream(content3), "file3"); + zipResult = postBoxZipFileWriter.getZipResult(); + } + assertThat("compressible files only containing zeroes should be compressed into one file", zipResult.getFileLengths().size(), is(1)); + } + + public static void extractSplitZipFiles(File[] zipFiles) throws IOException { + byte[] buffer = new byte[ZIP_BUFFER_SIZE_IN_BYTE]; + for (File zipFilePath : zipFiles) { + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath))) { + ZipEntry zipEntry = zis.getNextEntry(); + + while (zipEntry != null) { + File newFile = new File(DESTINATION_DIR + "/" + zipEntry.getName()); + new File(newFile.getParent()).mkdirs(); // Create directories for nested files + try (FileOutputStream fos = new FileOutputStream(newFile, true)) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + zipEntry = zis.getNextEntry(); + } + } + } + } +}