From d33ade5d31ba74e109c8d69bf8bb860d3ec7a074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eivind=20Bergst=C3=B8l?= Date: Thu, 13 Jun 2024 12:30:09 +0200 Subject: [PATCH 1/3] Add InputStreamIterator This iterator wrapps an InputStream and reads chunks from it returning the chunks in next() if i has more chunks. I added two constructors. One that takes the chunk size for bytes and one that uses DataSize. With DataSize bytes or kiloBytes only should be used, since too big chunks would defeat the purpose of this iterator. Its better to read it fully to a byte[]. DataSize uses long, and thus numbers bigger that Integer.MAX will result in a failing iterator. --- .../no/digipost/io/InputStreamIterator.java | 112 ++++++++++++++++++ .../digipost/io/InputStreamIteratorTest.java | 108 +++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/main/java/no/digipost/io/InputStreamIterator.java create mode 100644 src/test/java/no/digipost/io/InputStreamIteratorTest.java diff --git a/src/main/java/no/digipost/io/InputStreamIterator.java b/src/main/java/no/digipost/io/InputStreamIterator.java new file mode 100644 index 0000000..da05311 --- /dev/null +++ b/src/main/java/no/digipost/io/InputStreamIterator.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) Posten Norge AS + * + * Licensed 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 no.digipost.io; + +import no.digipost.DiggBase; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * InputStreamIterator is an {@link Iterator} reading from an {@link InputStream} in chunks + * where each chunk is returned as the next element in the iterable. + * When the input stream is fully consumed the iterator has no more elements. + */ +public class InputStreamIterator implements Iterator { + private final InputStream inputStream; + private final int chunkSize; + private byte[] next; + private Boolean hasNext; + private boolean endOfStreamReached = false; + + /** + * @param inputStream The input stream to iterate over + * @param chunkSize DataSize should not be too big since that defeats the purpose of this iterator. + */ + public InputStreamIterator(InputStream inputStream, DataSize chunkSize) { + this.inputStream = inputStream; + this.chunkSize = (int) chunkSize.toBytes(); + } + + public InputStreamIterator(InputStream inputStream, int chunkSizeBytes) { + this.inputStream = inputStream; + this.chunkSize = chunkSizeBytes; + } + + private byte[] loadNextChunk() { + if (endOfStreamReached) return null; + + byte[] chunk = new byte[chunkSize]; + int bytesRead = 0; + try { + bytesRead = inputStream.read(chunk); + if (bytesRead == -1) { + endOfStreamReached = true; + return null; + } + } catch (IOException e) { + throw new WrappedInputStreamFailed(e, inputStream); + } + + if (bytesRead < chunkSize) { + // resize the buffer if less data was read + byte[] smallerBuffer = new byte[bytesRead]; + System.arraycopy(chunk, 0, smallerBuffer, 0, bytesRead); + chunk = smallerBuffer; + } + + return chunk; + } + + /** + * If the iterator fails reading from the wrapped InputStream an + * {@link InputStreamIterator.WrappedInputStreamFailed} runtime exception is thrown. + * + * @return true if the iteration has more elements + */ + @Override + public boolean hasNext() { + if (hasNext == null) { + next = loadNextChunk(); + hasNext = (next != null); + } + + return hasNext; + } + + @Override + public byte[] next() { + if (!hasNext()) { + throw new NoSuchElementException("No more data to read"); + } + + byte[] result = next; + hasNext = null; + next = null; + return result; + } + + public static final class WrappedInputStreamFailed extends RuntimeException { + private static final long serialVersionUID = 1L; + + private WrappedInputStreamFailed(Throwable cause, InputStream inputStream) { + super("The InputStream " + DiggBase.friendlyName(inputStream.getClass()) + + " read failed. Cause: " + cause.getClass() + ": " + cause.getMessage(), cause); + } + } +} diff --git a/src/test/java/no/digipost/io/InputStreamIteratorTest.java b/src/test/java/no/digipost/io/InputStreamIteratorTest.java new file mode 100644 index 0000000..0eeef90 --- /dev/null +++ b/src/test/java/no/digipost/io/InputStreamIteratorTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) Posten Norge AS + * + * Licensed 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 no.digipost.io; + +import no.digipost.io.InputStreamIterator.WrappedInputStreamFailed; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.NoSuchElementException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static uk.co.probablyfine.matchers.Java8Matchers.where; + +class InputStreamIteratorTest { + + @Test + void should_read_the_input_stream_fully() throws Exception { + StringBuilder sb = new StringBuilder(); + + try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { + InputStreamIterator iterator = new InputStreamIterator(inputStream, 2); + + while (iterator.hasNext()) { + sb.append(new String(iterator.next())); + } + } + + assertEquals("Some data", sb.toString()); + } + + @Test + void should_read_the_input_stream_fully_with_datasize() throws Exception { + StringBuilder sb = new StringBuilder(); + + try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { + + InputStreamIterator iterator = new InputStreamIterator(inputStream, DataSize.bytes(2)); + while (iterator.hasNext()) { + sb.append(new String(iterator.next())); + } + } + + assertEquals("Some data", sb.toString()); + } + + @Test + void too_big_data_size_will_throw_NegativeArraySizeException() throws Exception { + try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8))) { + InputStreamIterator iterator = new InputStreamIterator(inputStream, DataSize.MAX); + + assertThrows(NegativeArraySizeException.class, iterator::hasNext); + } + } + + @Test + void should_throw_if_next_is_called_with_no_more_elements() throws Exception { + StringBuilder sb = new StringBuilder(); + + try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { + + InputStreamIterator iterator = new InputStreamIterator(inputStream, 2); + + while (iterator.hasNext()) { + sb.append(new String(iterator.next())); + } + + assertThrows(NoSuchElementException.class, iterator::next); + } + + assertEquals("Some data", sb.toString()); + } + + @Test + void should_throw_exception_if_input_stream_fails() throws Exception { + try (final InputStream failingInputStream = new InputStream() { + + @Override + public int read() throws IOException { + throw new IOException("This input stream is broken"); + } + }) { + InputStreamIterator iterator = new InputStreamIterator(failingInputStream, 1); + + final WrappedInputStreamFailed ex = assertThrows(WrappedInputStreamFailed.class, iterator::next); + assertThat(ex, where(Exception::getMessage, containsString("InputStreamIteratorTest."))); + } + + } +} From 82851d75111deeea545faffe5c7d285e0ca41eb4 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 13 Jun 2024 16:20:09 +0200 Subject: [PATCH 2/3] Add test for inputstreams with multiple entries Basically to verify that InputStreamIterator will not decide to close its wrapped InputStream. The rationaly for this can be found in this arbitrarily chosen article: https://www.bekk.christmas/post/2019/13/good-ol'-io-streams --- .../digipost/io/InputStreamIteratorTest.java | 101 +++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/src/test/java/no/digipost/io/InputStreamIteratorTest.java b/src/test/java/no/digipost/io/InputStreamIteratorTest.java index 0eeef90..d044c62 100644 --- a/src/test/java/no/digipost/io/InputStreamIteratorTest.java +++ b/src/test/java/no/digipost/io/InputStreamIteratorTest.java @@ -16,15 +16,26 @@ package no.digipost.io; import no.digipost.io.InputStreamIterator.WrappedInputStreamFailed; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -99,10 +110,98 @@ public int read() throws IOException { } }) { InputStreamIterator iterator = new InputStreamIterator(failingInputStream, 1); - + final WrappedInputStreamFailed ex = assertThrows(WrappedInputStreamFailed.class, iterator::next); assertThat(ex, where(Exception::getMessage, containsString("InputStreamIteratorTest."))); } } + + @Test + void worksWithInputStreamHavingMultipleEntries() throws IOException { + ZipEntryContent file1 = new ZipEntryContent("file1.txt", "This is file1"); + ZipEntryContent file2 = new ZipEntryContent("file2.txt", "This is file2"); + byte[] zipFile = zip(file1, file2); + + + List entriesReadConventionally = new ArrayList<>(); + try (ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(zipFile))) { + for (ZipEntry nextEntry = zipReader.getNextEntry(); nextEntry != null; nextEntry = zipReader.getNextEntry()) { + entriesReadConventionally.add(ZipEntryContent.read(nextEntry, zipReader)); + } + } + + assertThat(entriesReadConventionally, containsInAnyOrder(file1, file2)); + + + List entriesReadInChunks = new ArrayList<>(); + try (ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(zipFile))) { + for (ZipEntry nextEntry = zipReader.getNextEntry(); nextEntry != null; nextEntry = zipReader.getNextEntry()) { + ByteArrayOutputStream entryConsumer = new ByteArrayOutputStream(); + for (byte[] chunk : (Iterable) () -> new InputStreamIterator(zipReader, DataSize.bytes(2))) { + entryConsumer.write(chunk); + } + entriesReadInChunks.add(ZipEntryContent.read(nextEntry, entryConsumer.toByteArray())); + } + } + + assertThat(entriesReadInChunks, containsInAnyOrder(file1, file2)); + } + + + private static final class ZipEntryContent { + static ZipEntryContent read(ZipEntry entry, byte[] content) throws IOException { + return read(entry, new ByteArrayInputStream(content)); + } + + static ZipEntryContent read(ZipEntry entry, InputStream contentStream) throws IOException { + return new ZipEntryContent(entry.getName(), IOUtils.toString(contentStream, UTF_8)); + } + + final String name; + final String content; + + ZipEntryContent(String name, String content) { + this.name = name; + this.content = content; + } + + public void writeTo(ZipOutputStream zip) throws IOException { + zip.putNextEntry(new ZipEntry(name)); + zip.write(content.getBytes(UTF_8)); + } + + @Override + public String toString() { + return "zip entry '" + name + "': " + content; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ZipEntryContent) { + ZipEntryContent that = (ZipEntryContent) o; + return Objects.equals(this.name, that.name) && Objects.equals(this.content, that.content); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(name, content); + } + + } + + private static byte[] zip(ZipEntryContent ... entries) { + ByteArrayOutputStream zipOutput = new ByteArrayOutputStream(); + try (ZipOutputStream zipWriter = new ZipOutputStream(zipOutput)) { + for (ZipEntryContent entry : entries) { + entry.writeTo(zipWriter); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return zipOutput.toByteArray(); + } + } From 524aa4919a55f0fb44017a9fc4cbd51e726a9ab0 Mon Sep 17 00:00:00 2001 From: Rune Flobakk Date: Thu, 13 Jun 2024 17:07:06 +0200 Subject: [PATCH 3/3] Some finishing touches Rethrow IOException as UncheckedIOException instead of introducing our own exception type. Remove redundant flag endOfStreamReached, it was actually never inspected, as the call to loadNextChunk was guarded by hasNext flag. Some kaizen refactoring of InputStreamIteratorTest. --- .../no/digipost/io/InputStreamIterator.java | 39 +++---- .../digipost/io/InputStreamIteratorTest.java | 104 +++++++----------- 2 files changed, 58 insertions(+), 85 deletions(-) diff --git a/src/main/java/no/digipost/io/InputStreamIterator.java b/src/main/java/no/digipost/io/InputStreamIterator.java index da05311..26dcff3 100644 --- a/src/main/java/no/digipost/io/InputStreamIterator.java +++ b/src/main/java/no/digipost/io/InputStreamIterator.java @@ -15,13 +15,16 @@ */ package no.digipost.io; -import no.digipost.DiggBase; - import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.util.Iterator; import java.util.NoSuchElementException; +import static java.lang.Math.toIntExact; +import static no.digipost.DiggBase.friendlyName; +import static no.digipost.DiggExceptions.exceptionNameAndMessage; + /** * InputStreamIterator is an {@link Iterator} reading from an {@link InputStream} in chunks * where each chunk is returned as the next element in the iterable. @@ -29,41 +32,39 @@ */ public class InputStreamIterator implements Iterator { private final InputStream inputStream; - private final int chunkSize; + private final int chunkSizeBytes; private byte[] next; private Boolean hasNext; - private boolean endOfStreamReached = false; /** * @param inputStream The input stream to iterate over * @param chunkSize DataSize should not be too big since that defeats the purpose of this iterator. */ public InputStreamIterator(InputStream inputStream, DataSize chunkSize) { - this.inputStream = inputStream; - this.chunkSize = (int) chunkSize.toBytes(); + this(inputStream, toIntExact(chunkSize.toBytes())); } public InputStreamIterator(InputStream inputStream, int chunkSizeBytes) { this.inputStream = inputStream; - this.chunkSize = chunkSizeBytes; + this.chunkSizeBytes = chunkSizeBytes; } private byte[] loadNextChunk() { - if (endOfStreamReached) return null; - - byte[] chunk = new byte[chunkSize]; + byte[] chunk = new byte[chunkSizeBytes]; int bytesRead = 0; try { bytesRead = inputStream.read(chunk); if (bytesRead == -1) { - endOfStreamReached = true; return null; } } catch (IOException e) { - throw new WrappedInputStreamFailed(e, inputStream); + throw new UncheckedIOException( + "Failed reading next chunk of up to " + chunkSizeBytes + + " bytes from " + friendlyName(inputStream.getClass()) + + " because " + exceptionNameAndMessage(e), e); } - if (bytesRead < chunkSize) { + if (bytesRead < chunkSizeBytes) { // resize the buffer if less data was read byte[] smallerBuffer = new byte[bytesRead]; System.arraycopy(chunk, 0, smallerBuffer, 0, bytesRead); @@ -74,10 +75,10 @@ private byte[] loadNextChunk() { } /** - * If the iterator fails reading from the wrapped InputStream an - * {@link InputStreamIterator.WrappedInputStreamFailed} runtime exception is thrown. * * @return true if the iteration has more elements + * + * @throws UncheckedIOException if the wrapped InputStream throws an IOException */ @Override public boolean hasNext() { @@ -101,12 +102,4 @@ public byte[] next() { return result; } - public static final class WrappedInputStreamFailed extends RuntimeException { - private static final long serialVersionUID = 1L; - - private WrappedInputStreamFailed(Throwable cause, InputStream inputStream) { - super("The InputStream " + DiggBase.friendlyName(inputStream.getClass()) + - " read failed. Cause: " + cause.getClass() + ": " + cause.getMessage(), cause); - } - } } diff --git a/src/test/java/no/digipost/io/InputStreamIteratorTest.java b/src/test/java/no/digipost/io/InputStreamIteratorTest.java index d044c62..060520c 100644 --- a/src/test/java/no/digipost/io/InputStreamIteratorTest.java +++ b/src/test/java/no/digipost/io/InputStreamIteratorTest.java @@ -15,8 +15,8 @@ */ package no.digipost.io; -import no.digipost.io.InputStreamIterator.WrappedInputStreamFailed; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BrokenInputStream; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; @@ -24,8 +24,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; @@ -34,87 +35,55 @@ import java.util.zip.ZipOutputStream; import static java.nio.charset.StandardCharsets.UTF_8; +import static no.digipost.DiggExceptions.runUnchecked; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.is; import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static uk.co.probablyfine.matchers.Java8Matchers.where; +import static uk.co.probablyfine.matchers.Java8Matchers.whereNot; class InputStreamIteratorTest { @Test - void should_read_the_input_stream_fully() throws Exception { - StringBuilder sb = new StringBuilder(); + void fully_reads_the_input_stream() throws Exception { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(UTF_8));) { + String consumedFromIterator = consumeToString(new InputStreamIterator(inputStream, DataSize.bytes(2)), UTF_8); - try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { - InputStreamIterator iterator = new InputStreamIterator(inputStream, 2); - - while (iterator.hasNext()) { - sb.append(new String(iterator.next())); - } - } - - assertEquals("Some data", sb.toString()); - } - - @Test - void should_read_the_input_stream_fully_with_datasize() throws Exception { - StringBuilder sb = new StringBuilder(); - - try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { - - InputStreamIterator iterator = new InputStreamIterator(inputStream, DataSize.bytes(2)); - while (iterator.hasNext()) { - sb.append(new String(iterator.next())); - } + assertThat(consumedFromIterator, is("Some data")); } - - assertEquals("Some data", sb.toString()); } @Test - void too_big_data_size_will_throw_NegativeArraySizeException() throws Exception { - try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8))) { - InputStreamIterator iterator = new InputStreamIterator(inputStream, DataSize.MAX); + void cannot_instantiate_with_too_big_chunk_size() throws Exception { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(UTF_8))) { + Exception thrown = assertThrows(ArithmeticException.class, () -> new InputStreamIterator(inputStream, DataSize.MAX)); - assertThrows(NegativeArraySizeException.class, iterator::hasNext); + assertThat(thrown, where(Exception::getMessage, containsStringIgnoringCase("integer overflow"))); } } @Test - void should_throw_if_next_is_called_with_no_more_elements() throws Exception { - StringBuilder sb = new StringBuilder(); - - try (final ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(StandardCharsets.UTF_8));) { - + void throws_if_next_is_called_with_no_more_elements() throws Exception { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream("Some data".getBytes(UTF_8));) { InputStreamIterator iterator = new InputStreamIterator(inputStream, 2); - while (iterator.hasNext()) { - sb.append(new String(iterator.next())); - } + assertThat(consumeToString(iterator, UTF_8), is("Some data")); + assertThat(iterator, whereNot(Iterator::hasNext)); assertThrows(NoSuchElementException.class, iterator::next); } - - assertEquals("Some data", sb.toString()); } @Test - void should_throw_exception_if_input_stream_fails() throws Exception { - try (final InputStream failingInputStream = new InputStream() { + void throws_exception_if_input_stream_fails() throws Exception { + InputStreamIterator iterator = new InputStreamIterator(new BrokenInputStream(), 3); - @Override - public int read() throws IOException { - throw new IOException("This input stream is broken"); - } - }) { - InputStreamIterator iterator = new InputStreamIterator(failingInputStream, 1); - - final WrappedInputStreamFailed ex = assertThrows(WrappedInputStreamFailed.class, iterator::next); - assertThat(ex, where(Exception::getMessage, containsString("InputStreamIteratorTest."))); - } + Exception ex = assertThrows(UncheckedIOException.class, iterator::next); + assertThat(ex, where(Exception::getMessage, containsString("BrokenInputStream"))); } @Test @@ -137,22 +106,29 @@ void worksWithInputStreamHavingMultipleEntries() throws IOException { List entriesReadInChunks = new ArrayList<>(); try (ZipInputStream zipReader = new ZipInputStream(new ByteArrayInputStream(zipFile))) { for (ZipEntry nextEntry = zipReader.getNextEntry(); nextEntry != null; nextEntry = zipReader.getNextEntry()) { - ByteArrayOutputStream entryConsumer = new ByteArrayOutputStream(); - for (byte[] chunk : (Iterable) () -> new InputStreamIterator(zipReader, DataSize.bytes(2))) { - entryConsumer.write(chunk); - } - entriesReadInChunks.add(ZipEntryContent.read(nextEntry, entryConsumer.toByteArray())); + String content = consumeToString(new InputStreamIterator(zipReader, DataSize.bytes(2)), UTF_8); + entriesReadInChunks.add(new ZipEntryContent(nextEntry, content)); } } assertThat(entriesReadInChunks, containsInAnyOrder(file1, file2)); } + private static String consumeToString(InputStreamIterator iterator, Charset charset) { + byte[] bytes = consumeAndFlatten(iterator); + return new String(bytes, charset); + } - private static final class ZipEntryContent { - static ZipEntryContent read(ZipEntry entry, byte[] content) throws IOException { - return read(entry, new ByteArrayInputStream(content)); + private static byte[] consumeAndFlatten(InputStreamIterator iterator) { + ByteArrayOutputStream chunkConsumer = new ByteArrayOutputStream(); + for (byte[] chunk : (Iterable) () -> iterator) { + runUnchecked(() -> chunkConsumer.write(chunk)); } + return chunkConsumer.toByteArray(); + } + + + private static final class ZipEntryContent { static ZipEntryContent read(ZipEntry entry, InputStream contentStream) throws IOException { return new ZipEntryContent(entry.getName(), IOUtils.toString(contentStream, UTF_8)); @@ -161,6 +137,10 @@ static ZipEntryContent read(ZipEntry entry, InputStream contentStream) throws IO final String name; final String content; + ZipEntryContent(ZipEntry entry, String content) { + this(entry.getName(), content); + } + ZipEntryContent(String name, String content) { this.name = name; this.content = content;