You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2023/03/01 09:43:25 UTC

[causeway] branch master updated: CAUSEWAY-3304: purge ZipReader in favor of new ZipUtils

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/master by this push:
     new 1a78934400 CAUSEWAY-3304: purge ZipReader in favor of new ZipUtils
1a78934400 is described below

commit 1a78934400a0b55de3e2bbdc377a671696e28272
Author: Andi Huber <ah...@apache.org>
AuthorDate: Wed Mar 1 10:43:20 2023 +0100

    CAUSEWAY-3304: purge ZipReader in favor of new ZipUtils
---
 .../modules/applib/pages/index/util/ZipReader.adoc |  16 ---
 .../modules/applib/pages/index/value/Blob.adoc     |  61 ++++++---
 .../modules/applib/pages/index/value/Clob.adoc     |  23 +++-
 .../modules/commons/pages/index/io/ZipUtils.adoc   |  38 ++++++
 .../HasUsername_recentExecutionsByUser.adoc        |   1 -
 .../org/apache/causeway/applib/util/ZipReader.java | 107 ----------------
 .../org/apache/causeway/applib/value/Blob.java     |  30 ++---
 .../org/apache/causeway/commons/io/ZipUtils.java   | 136 +++++++++++++++++++++
 core/adoc/modules/_overview/pages/about.adoc       |   4 +-
 9 files changed, 243 insertions(+), 173 deletions(-)

diff --git a/antora/components/refguide-index/modules/applib/pages/index/util/ZipReader.adoc b/antora/components/refguide-index/modules/applib/pages/index/util/ZipReader.adoc
deleted file mode 100644
index 63aecdc39a..0000000000
--- a/antora/components/refguide-index/modules/applib/pages/index/util/ZipReader.adoc
+++ /dev/null
@@ -1,16 +0,0 @@
-= ZipReader
-:Notice: 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 ag [...]
-
-Zip utility for processing compressed input.
-
-== API
-
-[source,java]
-.ZipReader.java
-----
-class ZipReader {
-  void read(InputStream inputStream, ZipVcausewaytor zipVcausewaytor)
-  Optional<R> digest(InputStream inputStream, ZipDigester<R> zipDigester)
-}
-----
-
diff --git a/antora/components/refguide-index/modules/applib/pages/index/value/Blob.adoc b/antora/components/refguide-index/modules/applib/pages/index/value/Blob.adoc
index 78eb030321..f950b4f11c 100644
--- a/antora/components/refguide-index/modules/applib/pages/index/value/Blob.adoc
+++ b/antora/components/refguide-index/modules/applib/pages/index/value/Blob.adoc
@@ -19,6 +19,7 @@ class Blob {
   Blob(String name, String mimeTypeBase, byte[] bytes)
   Blob(String name, MimeType mimeType, byte[] bytes)
   Blob of(String name, CommonMimeType mimeType, byte[] content)     // <.>
+  Try<Blob> tryRead(String name, CommonMimeType mimeType, DataSource dataSource)     // <.>
   Try<Blob> tryRead(String name, CommonMimeType mimeType, File file)     // <.>
   String getName()
   MimeType getMimeType()
@@ -26,9 +27,9 @@ class Blob {
   Clob toClob(Charset charset)     // <.>
   void writeBytesTo(OutputStream os)     // <.>
   void writeTo(File file)     // <.>
-  void consume(Consumer<InputStream> consumer)     // <.>
-  R digest(Function<InputStream, R> digester)     // <.>
-  Blob zip()
+  DataSource asDataSource()     // <.>
+  Blob zip()     // <.>
+  Blob zip(String zipEntryNameIfAny)     // <.>
   Blob unZip(CommonMimeType resultingMimeType)
   boolean equals(Object o)
   int hashCode()
@@ -42,10 +43,15 @@ class Blob {
 --
 Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and _content_ .
 --
+<.> xref:#tryRead_String_CommonMimeType_DataSource[tryRead(String, CommonMimeType, DataSource)]
++
+--
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and content from _dataSource_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+--
 <.> xref:#tryRead_String_CommonMimeType_File[tryRead(String, CommonMimeType, File)]
 +
 --
-Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and content from _file_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+Shortcut for _tryRead(name, mimeType, DataSource.ofFile(file))_
 --
 <.> xref:#toClob_Charset[toClob(Charset)]
 +
@@ -62,15 +68,20 @@ Does not close the OutputStream.
 --
 Writes this xref:refguide:applib:index/value/Blob.adoc[Blob] to the file represented by the specified `File` object.
 --
-<.> xref:#consume_Consumer[consume(Consumer)]
+<.> xref:#asDataSource_[asDataSource()]
++
+--
+Returns a new xref:refguide:commons:index/io/DataSource.adoc[DataSource] for underlying byte array.
+--
+<.> xref:#zip_[zip()]
 +
 --
-The _InputStream_ involved is closed after consumption.
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] that has this Blob's underlying byte array zipped into a zip-entry using this Blob's name.
 --
-<.> xref:#digest_Function[digest(Function)]
+<.> xref:#zip_String[zip(String)]
 +
 --
-The _InputStream_ involved is closed after digestion.
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] that has this Blob's underlying byte array zipped into a zip-entry with given zip-entry name.
 --
 <.> xref:#asImage_[asImage()]
 
@@ -81,18 +92,23 @@ The _InputStream_ involved is closed after digestion.
 
 Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and _content_ .
 
-_name_ may or may not include the desired filename extension, it is guaranteed, that the resulting xref:refguide:applib:index/value/Blob.adoc[Blob] has the appropriate extension as constraint by the given _mimeType_ .
+_name_ may or may not include the desired filename extension, as it is guaranteed, that the resulting xref:refguide:applib:index/value/Blob.adoc[Blob] has the appropriate extension as constraint by the given _mimeType_ .
 
 For more fine-grained control use one of the xref:refguide:applib:index/value/Blob.adoc[Blob] constructors directly.
 
-[#tryRead_String_CommonMimeType_File]
-=== tryRead(String, CommonMimeType, File)
+[#tryRead_String_CommonMimeType_DataSource]
+=== tryRead(String, CommonMimeType, DataSource)
 
-Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and content from _file_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] of given _name_ , _mimeType_ and content from _dataSource_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
 
-_name_ may or may not include the desired filename extension, it is guaranteed, that the resulting xref:refguide:applib:index/value/Blob.adoc[Blob] has the appropriate extension as constraint by the given _mimeType_ .
+_name_ may or may not include the desired filename extension, as it is guaranteed, that the resulting xref:refguide:applib:index/value/Blob.adoc[Blob] has the appropriate extension as constraint by the given _mimeType_ .
 
-For more fine-grained control use one of the xref:refguide:applib:index/value/Blob.adoc[Blob] constructors directly.
+For more fine-grained control use one of the xref:refguide:applib:index/value/Blob.adoc[Blob] factories directly.
+
+[#tryRead_String_CommonMimeType_File]
+=== tryRead(String, CommonMimeType, File)
+
+Shortcut for _tryRead(name, mimeType, DataSource.ofFile(file))_
 
 [#toClob_Charset]
 === toClob(Charset)
@@ -111,15 +127,20 @@ Writes this xref:refguide:applib:index/value/Blob.adoc[Blob] to the file represe
 
 If the file exists but is a directory rather than a regular file, does not exist but cannot be created, or cannot be opened for any other reason then a `FileNotFoundException` is thrown.
 
-[#consume_Consumer]
-=== consume(Consumer)
+[#asDataSource_]
+=== asDataSource()
+
+Returns a new xref:refguide:commons:index/io/DataSource.adoc[DataSource] for underlying byte array.
+
+[#zip_]
+=== zip()
 
-The _InputStream_ involved is closed after consumption.
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] that has this Blob's underlying byte array zipped into a zip-entry using this Blob's name.
 
-[#digest_Function]
-=== digest(Function)
+[#zip_String]
+=== zip(String)
 
-The _InputStream_ involved is closed after digestion.
+Returns a new xref:refguide:applib:index/value/Blob.adoc[Blob] that has this Blob's underlying byte array zipped into a zip-entry with given zip-entry name.
 
 [#asImage_]
 === asImage()
diff --git a/antora/components/refguide-index/modules/applib/pages/index/value/Clob.adoc b/antora/components/refguide-index/modules/applib/pages/index/value/Clob.adoc
index a3ba529905..f2a6527a77 100644
--- a/antora/components/refguide-index/modules/applib/pages/index/value/Clob.adoc
+++ b/antora/components/refguide-index/modules/applib/pages/index/value/Clob.adoc
@@ -22,6 +22,7 @@ class Clob {
   Clob(String name, String mimeTypeBase, CharSequence chars)
   Clob(String name, MimeType mimeType, CharSequence chars)
   Clob of(String name, CommonMimeType mimeType, CharSequence content)     // <.>
+  Try<Clob> tryRead(String name, CommonMimeType mimeType, DataSource dataSource, Charset charset)     // <.>
   Try<Clob> tryRead(String name, CommonMimeType mimeType, File file, Charset charset)     // <.>
   Try<Clob> tryReadUtf8(String name, CommonMimeType mimeType, File file)     // <.>
   String getName()
@@ -44,10 +45,15 @@ class Clob {
 --
 Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and _content_ .
 --
+<.> xref:#tryRead_String_CommonMimeType_DataSource_Charset[tryRead(String, CommonMimeType, DataSource, Charset)]
++
+--
+Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and content from _dataSource_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+--
 <.> xref:#tryRead_String_CommonMimeType_File_Charset[tryRead(String, CommonMimeType, File, Charset)]
 +
 --
-Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and content from _file_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+Shortcut for _tryRead(name, mimeType, DataSource.ofFile(file), charset)_
 --
 <.> xref:#tryReadUtf8_String_CommonMimeType_File[tryReadUtf8(String, CommonMimeType, File)]
 +
@@ -82,19 +88,24 @@ Shortcut for _#writeTo(File, Charset)_ using _StandardCharsets#UTF_8_ .
 
 Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and _content_ .
 
-_name_ may or may not include the desired filename extension, it is guaranteed, that the resulting xref:refguide:applib:index/value/Clob.adoc[Clob] has the appropriate extension as constraint by the given _mimeType_ .
+_name_ may or may not include the desired filename extension, as it is guaranteed, that the resulting xref:refguide:applib:index/value/Clob.adoc[Clob] has the appropriate extension as constraint by the given _mimeType_ .
 
 For more fine-grained control use one of the xref:refguide:applib:index/value/Clob.adoc[Clob] constructors directly.
 
-[#tryRead_String_CommonMimeType_File_Charset]
-=== tryRead(String, CommonMimeType, File, Charset)
+[#tryRead_String_CommonMimeType_DataSource_Charset]
+=== tryRead(String, CommonMimeType, DataSource, Charset)
 
-Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and content from _file_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
+Returns a new xref:refguide:applib:index/value/Clob.adoc[Clob] of given _name_ , _mimeType_ and content from _dataSource_ , wrapped with a xref:refguide:commons:index/functional/Try.adoc[Try] .
 
-_name_ may or may not include the desired filename extension, it is guaranteed, that the resulting xref:refguide:applib:index/value/Clob.adoc[Clob] has the appropriate extension as constraint by the given _mimeType_ .
+_name_ may or may not include the desired filename extension, as it is guaranteed, that the resulting xref:refguide:applib:index/value/Clob.adoc[Clob] has the appropriate extension as constraint by the given _mimeType_ .
 
 For more fine-grained control use one of the xref:refguide:applib:index/value/Clob.adoc[Clob] constructors directly.
 
+[#tryRead_String_CommonMimeType_File_Charset]
+=== tryRead(String, CommonMimeType, File, Charset)
+
+Shortcut for _tryRead(name, mimeType, DataSource.ofFile(file), charset)_
+
 [#tryReadUtf8_String_CommonMimeType_File]
 === tryReadUtf8(String, CommonMimeType, File)
 
diff --git a/antora/components/refguide-index/modules/commons/pages/index/io/ZipUtils.adoc b/antora/components/refguide-index/modules/commons/pages/index/io/ZipUtils.adoc
new file mode 100644
index 0000000000..33db4d9522
--- /dev/null
+++ b/antora/components/refguide-index/modules/commons/pages/index/io/ZipUtils.adoc
@@ -0,0 +1,38 @@
+= ZipUtils
+:Notice: 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 ag [...]
+
+Utilities to zip and unzip data.
+
+== API
+
+[source,java]
+.ZipUtils.java
+----
+class ZipUtils {
+  Stream<ZipEntryDataSource> streamZipEntries(DataSource zippedSource, ZipOptions zipOptions)     // <.>
+  Stream<ZipEntryDataSource> streamZipEntries(DataSource zippedSource)     // <.>
+}
+----
+
+<.> xref:#streamZipEntries_DataSource_ZipOptions[streamZipEntries(DataSource, ZipOptions)]
++
+--
+Returns a _Stream_ of _ZipEntryDataSource_ , buffered in memory, which allows consumption even after the underlying zipped xref:refguide:commons:index/io/DataSource.adoc[DataSource] was closed.
+--
+<.> xref:#streamZipEntries_DataSource[streamZipEntries(DataSource)]
++
+--
+Shortcut for _streamZipEntries(zippedSource, ZipOptions.builder().build())_
+--
+
+== Members
+
+[#streamZipEntries_DataSource_ZipOptions]
+=== streamZipEntries(DataSource, ZipOptions)
+
+Returns a _Stream_ of _ZipEntryDataSource_ , buffered in memory, which allows consumption even after the underlying zipped xref:refguide:commons:index/io/DataSource.adoc[DataSource] was closed.
+
+[#streamZipEntries_DataSource]
+=== streamZipEntries(DataSource)
+
+Shortcut for _streamZipEntries(zippedSource, ZipOptions.builder().build())_
diff --git a/antora/components/refguide-index/modules/extensions/pages/index/executionlog/applib/contributions/HasUsername_recentExecutionsByUser.adoc b/antora/components/refguide-index/modules/extensions/pages/index/executionlog/applib/contributions/HasUsername_recentExecutionsByUser.adoc
index aff44d3894..60f2b98553 100644
--- a/antora/components/refguide-index/modules/extensions/pages/index/executionlog/applib/contributions/HasUsername_recentExecutionsByUser.adoc
+++ b/antora/components/refguide-index/modules/extensions/pages/index/executionlog/applib/contributions/HasUsername_recentExecutionsByUser.adoc
@@ -11,7 +11,6 @@ For example the _secman_ extension's `ApplicationUser` entity implements this in
 .HasUsername_recentExecutionsByUser.java
 ----
 class HasUsername_recentExecutionsByUser {
-  HasUsername_recentExecutionsByUser(HasUsername hasUsername)
   List<? extends ExecutionLogEntry> coll()
   boolean hideColl()
 }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/util/ZipReader.java b/api/applib/src/main/java/org/apache/causeway/applib/util/ZipReader.java
deleted file mode 100644
index f8e232b6ca..0000000000
--- a/api/applib/src/main/java/org/apache/causeway/applib/util/ZipReader.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- *  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.causeway.applib.util;
-
-import java.io.BufferedInputStream;
-import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
-import java.util.function.BiFunction;
-import java.util.function.BiPredicate;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-import org.springframework.lang.Nullable;
-
-import lombok.NonNull;
-import lombok.SneakyThrows;
-import lombok.val;
-
-/**
- * Zip utility for processing compressed input.
- * 
- * @since 2.0 {@index}
- * @see ZipWriter
- */
-public class ZipReader {
-    
-    private static final Charset ENTRY_NAME_CHARSET = StandardCharsets.UTF_8;
-    
-    /**
-     * BiPredicate stating whether to continue visiting after consuming {@link ZipEntry}.
-     * <p>
-     * The passed in {@link ZipInputStream} corresponds to given {@link ZipEntry} and must not be closed.
-     */
-    public static interface ZipVcausewaytor extends BiPredicate<ZipEntry, ZipInputStream> {
-    }
-    
-    /**
-     * BiFunction that stops visiting after the result is non-null for given {@link ZipEntry}.
-     * <p>
-     * The passed in {@link ZipInputStream} corresponds to given {@link ZipEntry} and must not be closed.
-     */
-    public static interface ZipDigester<R> extends BiFunction<ZipEntry, ZipInputStream, R> {
-    }
-
-    @SneakyThrows
-    public static void read(
-            final @Nullable InputStream inputStream, 
-            final @NonNull ZipVcausewaytor zipVcausewaytor) {
-        
-        if(inputStream==null) {
-            return; // no-op
-        }
-        
-        try(ZipInputStream in = new ZipInputStream(new BufferedInputStream(inputStream, 64*1024), ENTRY_NAME_CHARSET)){
-            ZipEntry entry;
-            while((entry=in.getNextEntry())!=null) {
-                if(!zipVcausewaytor.test(entry, in)) {
-                    return; // break request from visitor
-                }
-            }
-        }
-    }
-
-    
-    
-    @SneakyThrows
-    public static <R> Optional<R> digest(
-            final @Nullable InputStream inputStream, 
-            final @NonNull ZipDigester<R> zipDigester) {
-        
-        if(inputStream==null) {
-            return Optional.empty();
-        }
-        
-        try(ZipInputStream in = new ZipInputStream(new BufferedInputStream(inputStream, 64*1024), ENTRY_NAME_CHARSET)){
-            ZipEntry entry;
-            while((entry=in.getNextEntry())!=null) {
-                val digest = zipDigester.apply(entry, in);
-                if(digest!=null) {
-                    return Optional.of(digest); 
-                }
-            }
-        }
-        
-        return Optional.empty();
-    }
-    
-    
-}
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/Blob.java b/api/applib/src/main/java/org/apache/causeway/applib/value/Blob.java
index 7a338e6ef9..9faf524719 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/Blob.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/Blob.java
@@ -21,7 +21,6 @@ package org.apache.causeway.applib.value;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
 import java.util.Arrays;
@@ -39,15 +38,14 @@ import org.springframework.lang.Nullable;
 import org.apache.causeway.applib.CausewayModuleApplib;
 import org.apache.causeway.applib.annotation.Value;
 import org.apache.causeway.applib.jaxb.PrimitiveJaxbAdapters;
-import org.apache.causeway.applib.util.ZipReader;
 import org.apache.causeway.applib.util.ZipWriter;
 import org.apache.causeway.commons.functional.Try;
-import org.apache.causeway.commons.internal.base._Bytes;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.commons.internal.image._Images;
 import org.apache.causeway.commons.io.DataSource;
+import org.apache.causeway.commons.io.ZipUtils;
 
 import lombok.NonNull;
 import lombok.SneakyThrows;
@@ -263,24 +261,14 @@ public final class Blob implements NamedWithMimeType {
     }
 
     public Blob unZip(final @NonNull CommonMimeType resultingMimeType) {
-        return asDataSource().tryReadAndApply(is->
-            ZipReader.digest(is, (zipEntry, zipInputStream)->{
-                if(zipEntry.isDirectory()) {
-                    return (Blob)null; // continue
-                }
-                final byte[] unzippedBytes;
-                try {
-                    unzippedBytes = _Bytes.of(zipInputStream);
-                } catch (IOException e) {
-                    throw _Exceptions
-                        .unrecoverable(e, "failed to read zip entry %s", zipEntry.getName());
-                }
-                return Blob.of(zipEntry.getName(), resultingMimeType, unzippedBytes);
-            })
-            .orElseThrow()
-        )
-        .mapEmptyToFailure()
-        .valueAsNonNullElseFail();
+        return ZipUtils.streamZipEntries(asDataSource())
+                .map(zipEntryDataSource->Blob.of(
+                        zipEntryDataSource.zipEntry().getName(),
+                        resultingMimeType,
+                        zipEntryDataSource.bytes()))
+                .findFirst() // assuming first entry is the one we want
+                .orElseThrow(()->_Exceptions
+                      .unrecoverable("failed to unzip blob, no entry found %s", getName()));
     }
 
     // -- OBJECT CONTRACT
diff --git a/commons/src/main/java/org/apache/causeway/commons/io/ZipUtils.java b/commons/src/main/java/org/apache/causeway/commons/io/ZipUtils.java
new file mode 100644
index 0000000000..4681325e87
--- /dev/null
+++ b/commons/src/main/java/org/apache/causeway/commons/io/ZipUtils.java
@@ -0,0 +1,136 @@
+/*
+ *  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.causeway.commons.io;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.commons.internal.functions._Predicates;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
+import lombok.val;
+import lombok.experimental.Accessors;
+import lombok.experimental.UtilityClass;
+
+/**
+ * Utilities to zip and unzip data.
+ *
+ * @since 2.0 {@index}
+ */
+@UtilityClass
+public class ZipUtils {
+
+    //XXX record candidate
+    @Builder
+    @Value @Accessors(fluent=true)
+    public static class ZipOptions {
+        @Builder.Default
+        private final int bufferSize = 64*1024; // 64k
+        /**
+         * The {@link java.nio.charset.Charset charset} to be
+         *        used to decode the ZIP entry name (ignored if the
+         *        <a href="package-summary.html#lang_encoding"> language
+         *        encoding bit</a> of the ZIP entry's general purpose bit
+         *        flag is set).
+         */
+        @Builder.Default @NonNull
+        private final Charset zipEntryCharset = StandardCharsets.UTF_8;
+
+        @Builder.Default @NonNull
+        private final Predicate<ZipEntry> zipEntryFilter = _Predicates.alwaysTrue();
+    }
+
+    //XXX record candidate
+    @RequiredArgsConstructor
+    public static class ZipEntryDataSource implements DataSource {
+        @Getter @Accessors(fluent=true)
+        private final ZipEntry zipEntry;
+        private final byte[] bytes;
+
+        @Override
+        public <T> Try<T> tryReadAll(@NonNull final Function<InputStream, Try<T>> consumingMapper) {
+            try {
+                try(val bis = new ByteArrayInputStream(bytes)){
+                    return consumingMapper.apply(bis);
+                }
+            } catch (Throwable e) {
+                return Try.failure(e);
+            }
+        }
+    }
+
+    /**
+     * Returns a {@link Stream} of {@link ZipEntryDataSource}, buffered in memory,
+     * which allows consumption even after the underlying zipped {@link DataSource} was closed.
+     * @implNote Only partly optimized for heap usage, as it just reads all pre-filtered data into memory,
+     *      but doing so, regardless of what is actually consumed later from the returned {@link Stream}.
+     */
+    public Stream<ZipEntryDataSource> streamZipEntries(
+            final DataSource zippedSource,
+            final ZipOptions zipOptions) {
+
+        val zipEntryDataSources = _Lists.<ZipEntryDataSource>newArrayList();
+
+        zippedSource.tryReadAndApply(is->{
+            try(final ZipInputStream in = new ZipInputStream(
+                    new BufferedInputStream(is, zipOptions.bufferSize()),
+                    zipOptions.zipEntryCharset())) {
+
+                ZipEntry zipEntry;
+                while((zipEntry = in.getNextEntry())!=null) {
+                    if(zipEntry.isDirectory()) continue;
+                    if(zipOptions.zipEntryFilter().test(zipEntry)) {
+                        zipEntryDataSources.add(
+                                new ZipEntryDataSource(zipEntry, DataSource.ofInputStreamSupplier(()->in).bytes()));
+                    }
+                }
+            }
+            return null;
+        })
+        .mapFailure(IOException::new)
+        .ifFailureFail();
+
+        return zipEntryDataSources.stream();
+    }
+
+    /**
+     * Shortcut for {@code streamZipEntries(zippedSource, ZipOptions.builder().build())}
+     * @see #streamZipEntries(DataSource, ZipOptions)
+     */
+    public Stream<ZipEntryDataSource> streamZipEntries(
+            final DataSource zippedSource) {
+        return streamZipEntries(zippedSource, ZipOptions.builder().build());
+    }
+
+}
diff --git a/core/adoc/modules/_overview/pages/about.adoc b/core/adoc/modules/_overview/pages/about.adoc
index 72ac67d750..560b08da84 100644
--- a/core/adoc/modules/_overview/pages/about.adoc
+++ b/core/adoc/modules/_overview/pages/about.adoc
@@ -1366,7 +1366,7 @@ org.yaml:snakeyaml:jar:<managed> +
 
 .Document Index Entries
 ****
-xref:refguide:commons:index/collections/Can.adoc[Can], xref:refguide:commons:index/collections/Cardinality.adoc[Cardinality], xref:refguide:commons:index/functional/Either.adoc[Either], xref:refguide:commons:index/functional/Railway.adoc[Railway], xref:refguide:commons:index/functional/ThrowingConsumer.adoc[ThrowingConsumer], xref:refguide:commons:index/functional/ThrowingRunnable.adoc[ThrowingRunnable], xref:refguide:commons:index/functional/ThrowingSupplier.adoc[ThrowingSupplier], xref [...]
+xref:refguide:commons:index/collections/Can.adoc[Can], xref:refguide:commons:index/collections/Cardinality.adoc[Cardinality], xref:refguide:commons:index/functional/Either.adoc[Either], xref:refguide:commons:index/functional/Railway.adoc[Railway], xref:refguide:commons:index/functional/ThrowingConsumer.adoc[ThrowingConsumer], xref:refguide:commons:index/functional/ThrowingRunnable.adoc[ThrowingRunnable], xref:refguide:commons:index/functional/ThrowingSupplier.adoc[ThrowingSupplier], xref [...]
 ****
 |===
 
@@ -1558,7 +1558,7 @@ org.apache.causeway.core:causeway-schema:jar:<managed> +
 
 .Document Index Entries
 ****
-xref:refguide:applib:index/CausewayModuleApplib.adoc[CausewayModuleApplib], xref:refguide:applib:index/CausewayModuleApplibChangeAndExecutionLoggers.adoc[CausewayModuleApplibChangeAndExecutionLoggers], xref:refguide:applib:index/CausewayModuleApplibMixins.adoc[CausewayModuleApplibMixins], xref:refguide:applib:index/Identifier.adoc[Identifier], xref:refguide:applib:index/ViewModel.adoc[ViewModel], xref:refguide:applib:index/annotation/Action.adoc[Action], xref:refguide:applib:index/annota [...]
+xref:refguide:applib:index/CausewayModuleApplib.adoc[CausewayModuleApplib], xref:refguide:applib:index/CausewayModuleApplibChangeAndExecutionLoggers.adoc[CausewayModuleApplibChangeAndExecutionLoggers], xref:refguide:applib:index/CausewayModuleApplibMixins.adoc[CausewayModuleApplibMixins], xref:refguide:applib:index/Identifier.adoc[Identifier], xref:refguide:applib:index/ViewModel.adoc[ViewModel], xref:refguide:applib:index/annotation/Action.adoc[Action], xref:refguide:applib:index/annota [...]
 ****
 
 |Apache Causeway Core - Code Gen (ByteBuddy)