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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git

commit 5829240bfd3531639767e40ddf9608b84d3b235c
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Fri Apr 18 10:37:38 2025 -0400

    Add 
org.apache.commons.compress.compressors.xz.XZCompressorInputStream.builder/Builder()
    
    - Add 
org.apache.commons.compress.compressors.xz.XZCompressorOutputStream.builder/Builder()
    - Add org.apache.commons.compress.compressors.xz.XZCompressorRoundtripTest
---
 pom.xml                                            |   5 +
 src/changes/changes.xml                            |   2 +
 .../compressors/CompressorStreamFactory.java       |  15 ++-
 .../compressors/xz/XZCompressorInputStream.java    | 119 ++++++++++++++++++---
 .../compressors/xz/XZCompressorOutputStream.java   |  86 ++++++++++++++-
 .../compressors/xz/XZCompressorRoundtripTest.java  | 117 ++++++++++++++++++++
 6 files changed, 327 insertions(+), 17 deletions(-)

diff --git a/pom.xml b/pom.xml
index 00e317295..4db05eb31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -210,6 +210,11 @@ Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, 
arj.
       <artifactId>commons-lang3</artifactId>
       <version>3.17.0</version>
     </dependency>    
+    <dependency>
+      <groupId>org.junit-pioneer</groupId>
+      <artifactId>junit-pioneer</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <scm>
     
<connection>scm:git:https://gitbox.apache.org/repos/asf/commons-compress.git</connection>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 86c2aaea6..0ee09ff17 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -112,6 +112,8 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="add" issue="COMPRESS-697" dev="ggregory" due-to="Fredrik 
Kjellberg, Gary Gregory">Move BitStream.nextBit() method to BitInputStream 
#663.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream.builder/Builder().</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream.builder/Builder().</action>
+      <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
org.apache.commons.compress.compressors.xz.XZCompressorInputStream.builder/Builder().</action>
+      <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
org.apache.commons.compress.compressors.xz.XZCompressorOutputStream.builder/Builder().</action>
       <!-- UPDATE -->
       <action type="update" dev="sebb">Bump Commons Parent from 79 to 
81</action>
       <action type="update" dev="ggregory" due-to="Dependabot, Gary 
Gregory">Bump org.apache.commons:commons-parent from 72 to 79 #563, #567, #574, 
#582, #587, #595.</action>
diff --git 
a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
 
b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
index c6b1b0993..c02a90097 100644
--- 
a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
+++ 
b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java
@@ -592,7 +592,12 @@ public CompressorInputStream 
createCompressorInputStream(final String name, fina
         }
         try {
             if (GZIP.equalsIgnoreCase(name)) {
-                return 
GzipCompressorInputStream.builder().setInputStream(in).setDecompressConcatenated(actualDecompressConcatenated).get();
+                // @formatter:off
+                return GzipCompressorInputStream.builder()
+                        .setInputStream(in)
+                        
.setDecompressConcatenated(actualDecompressConcatenated)
+                        .get();
+                // @formatter:on
             }
             if (BZIP2.equalsIgnoreCase(name)) {
                 return new BZip2CompressorInputStream(in, 
actualDecompressConcatenated);
@@ -607,7 +612,13 @@ public CompressorInputStream 
createCompressorInputStream(final String name, fina
                 if (!XZUtils.isXZCompressionAvailable()) {
                     throw new CompressorException("XZ compression is not 
available." + YOU_NEED_XZ_JAVA);
                 }
-                return new XZCompressorInputStream(in, 
actualDecompressConcatenated, memoryLimitInKb);
+                // @formatter:off
+                return XZCompressorInputStream.builder()
+                        .setInputStream(in)
+                        
.setDecompressConcatenated(actualDecompressConcatenated)
+                        .setMemoryLimitKiB(memoryLimitInKb)
+                        .get();
+                // @formatter:on
             }
             if (ZSTANDARD.equalsIgnoreCase(name)) {
                 if (!ZstdUtils.isZstdCompressionAvailable()) {
diff --git 
a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java
 
b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java
index 9b7a88ef9..de7dc24c3 100644
--- 
a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java
+++ 
b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java
@@ -23,19 +23,103 @@
 
 import org.apache.commons.compress.MemoryLimitException;
 import org.apache.commons.compress.compressors.CompressorInputStream;
+import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
+import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
 import org.apache.commons.compress.utils.InputStreamStatistics;
+import org.apache.commons.io.build.AbstractStreamBuilder;
 import org.apache.commons.io.input.BoundedInputStream;
+import org.tukaani.xz.LZMA2Options;
 import org.tukaani.xz.SingleXZInputStream;
 import org.tukaani.xz.XZ;
 import org.tukaani.xz.XZInputStream;
 
+// @formatter:off
 /**
  * XZ decompressor.
- *
+ * <p>
+ * For example:
+ * </p>
+ * <pre>{@code
+ * XZCompressorInputStream s = XZCompressorInputStream.builder()
+ *   .setPath(path)
+ *   .setDecompressConcatenated(false)
+ *   .setMemoryLimitKiB(-1)
+ *   .get();
+ * }
+ * </pre>
  * @since 1.4
  */
+// @formatter:on
 public class XZCompressorInputStream extends CompressorInputStream implements 
InputStreamStatistics {
 
+    // @formatter:off
+    /**
+     * Builds a new {@link LZMACompressorInputStream}.
+     *
+     * <p>
+     * For example:
+     * </p>
+     * <pre>{@code
+     * XZCompressorInputStream s = XZCompressorInputStream.builder()
+     *   .setPath(path)
+     *   .setDecompressConcatenated(false)
+     *   .setMemoryLimitKiB(-1)
+     *   .get();
+     * }
+     * </pre>
+     *
+     * @see #get()
+     * @see LZMA2Options
+     * @since 1.28.0
+     */
+    // @formatter:on
+    public static class Builder extends 
AbstractStreamBuilder<XZCompressorInputStream, Builder> {
+
+        private int memoryLimitKiB = -1;
+        private boolean decompressConcatenated;
+
+        @Override
+        public XZCompressorInputStream get() throws IOException {
+            return new XZCompressorInputStream(this);
+        }
+
+        /**
+         * Whether to decompress until the end of the input.
+         *
+         * @param decompressConcatenated if true, decompress until the end of 
the input; if false, stop after the first .xz stream and leave the input 
position
+         *                               to point to the next byte after the 
.xz stream
+         * @return this instance.
+         */
+        public Builder setDecompressConcatenated(final boolean 
decompressConcatenated) {
+            this.decompressConcatenated = decompressConcatenated;
+            return this;
+        }
+
+
+        /**
+         * Sets a working memory threshold in kibibytes (KiB).
+         *
+         * @param memoryLimitKiB The memory limit used when reading blocks. 
The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to impose 
no
+         *                       memory usage limit. If the estimated memory 
limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is thrown.
+         * @return this instance.
+         */
+        public Builder setMemoryLimitKiB(final int memoryLimitKiB) {
+            this.memoryLimitKiB = memoryLimitKiB;
+            return this;
+        }
+    }
+
+    /**
+     * Constructs a new builder of {@link LZMACompressorOutputStream}.
+     *
+     * @return a new builder of {@link LZMACompressorOutputStream}.
+     * @since 1.28.0
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+
     /**
      * Checks if the signature matches what is expected for a .xz file.
      *
@@ -61,6 +145,16 @@ public static boolean matches(final byte[] signature, final 
int length) {
 
     private final InputStream in;
 
+    @SuppressWarnings("resource") // Caller closes
+    private XZCompressorInputStream(final Builder builder) throws IOException {
+        countingStream = 
BoundedInputStream.builder().setInputStream(builder.getInputStream()).get();
+        if (builder.decompressConcatenated) {
+            in = new XZInputStream(countingStream, builder.memoryLimitKiB);
+        } else {
+            in = new SingleXZInputStream(countingStream, 
builder.memoryLimitKiB);
+        }
+    }
+
     /**
      * Creates a new input stream that decompresses XZ-compressed data from 
the specified input stream. This doesn't support concatenated .xz files.
      *
@@ -69,7 +163,7 @@ public static boolean matches(final byte[] signature, final 
int length) {
      *                     this implementation, or the underlying {@code 
inputStream} throws an exception
      */
     public XZCompressorInputStream(final InputStream inputStream) throws 
IOException {
-        this(inputStream, false);
+        this(builder().setInputStream(inputStream));
     }
 
     /**
@@ -81,9 +175,11 @@ public XZCompressorInputStream(final InputStream 
inputStream) throws IOException
      *
      * @throws IOException if the input is not in the .xz format, the input is 
corrupt or truncated, the .xz headers specify options that are not supported by
      *                     this implementation, or the underlying {@code 
inputStream} throws an exception
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     public XZCompressorInputStream(final InputStream inputStream, final 
boolean decompressConcatenated) throws IOException {
-        this(inputStream, decompressConcatenated, -1);
+        
this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated));
     }
 
     /**
@@ -92,23 +188,22 @@ public XZCompressorInputStream(final InputStream 
inputStream, final boolean deco
      * @param inputStream            where to read the compressed data
      * @param decompressConcatenated if true, decompress until the end of the 
input; if false, stop after the first .xz stream and leave the input position to
      *                               point to the next byte after the .xz 
stream
-     * @param memoryLimitInKb        memory limit used when reading blocks. If 
the estimated memory limit is exceeded on {@link #read()}, a
-     *                               {@link MemoryLimitException} is thrown.
+     * @param memoryLimitKiB         The memory limit used when reading 
blocks. The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to 
impose
+     *                               no memory usage limit. If the estimated 
memory limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is
+     *                               thrown.
      *
      * @throws IOException if the input is not in the .xz format, the input is 
corrupt or truncated, the .xz headers specify options that are not supported by
      *                     this implementation, or the underlying {@code 
inputStream} throws an exception
      *
+     * @deprecated Use {@link #builder()}.
      * @since 1.14
      */
-    public XZCompressorInputStream(final InputStream inputStream, final 
boolean decompressConcatenated, final int memoryLimitInKb) throws IOException {
-        countingStream = 
BoundedInputStream.builder().setInputStream(inputStream).get();
-        if (decompressConcatenated) {
-            in = new XZInputStream(countingStream, memoryLimitInKb);
-        } else {
-            in = new SingleXZInputStream(countingStream, memoryLimitInKb);
-        }
+    @Deprecated
+    public XZCompressorInputStream(final InputStream inputStream, final 
boolean decompressConcatenated, final int memoryLimitKiB) throws IOException {
+        
this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated).setMemoryLimitKiB(memoryLimitKiB));
     }
 
+
     @Override
     public int available() throws IOException {
         return in.available();
diff --git 
a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java
 
b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java
index f59958da1..23a04d60f 100644
--- 
a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java
+++ 
b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java
@@ -22,13 +22,25 @@
 import java.io.OutputStream;
 
 import org.apache.commons.compress.compressors.CompressorOutputStream;
+import org.apache.commons.io.build.AbstractStreamBuilder;
 import org.tukaani.xz.LZMA2Options;
 import org.tukaani.xz.XZOutputStream;
 
+// @formatter:off
 /**
  * Compresses an output stream using the XZ and LZMA2 compression options.
+ * <p>
+ * For example:
+ * </p>
+ * <pre>{@code
+ * XZCompressorOutputStream s = XZCompressorOutputStream.builder()
+ *   .setPath(path)
+ *   .setLzma2Options(new LZMA2Options(...))
+ *   .get();
+ * }
+ * </pre>
  *
- * <em>Calling flush()</em>
+ * <h2>Calling flush</h2>
  * <p>
  * Calling {@link #flush()} flushes the encoder and calls {@code 
outputStream.flush()}. All buffered pending data will then be decompressible 
from the output
  * stream. Calling this function very often may increase the compressed file 
size a lot.
@@ -36,17 +48,83 @@
  *
  * @since 1.4
  */
+// @formatter:on
 public class XZCompressorOutputStream extends 
CompressorOutputStream<XZOutputStream> {
 
+    // @formatter:off
+    /**
+     * Builds a new {@link XZCompressorOutputStream}.
+     *
+     * <p>
+     * For example:
+     * </p>
+     * <pre>{@code
+     * XZCompressorOutputStream s = XZCompressorOutputStream.builder()
+     *   .setPath(path)
+     *   .setLzma2Options(new LZMA2Options(...))
+     *   .get();
+     * }
+     * </pre>
+     *
+     * @see #get()
+     * @since 1.28.0
+     */
+    // @formatter:on
+    public static class Builder extends 
AbstractStreamBuilder<XZCompressorOutputStream, Builder> {
+
+        private LZMA2Options lzma2Options = new LZMA2Options();
+
+        /**
+         * Constructs a new builder of {@link XZCompressorOutputStream}.
+         */
+        public Builder() {
+            // empty
+        }
+
+        @Override
+        public XZCompressorOutputStream get() throws IOException {
+            return new XZCompressorOutputStream(this);
+        }
+
+        /**
+         * Sets LZMA options.
+         * <p>
+         * Passing {@code null} resets to the default value {@link 
LZMA2Options#LZMA2Options()}.
+         * </p>
+         *
+         * @param lzma2Options LZMA options.
+         * @return this instance.
+         */
+        public Builder setLzma2Options(final LZMA2Options lzma2Options) {
+            this.lzma2Options = lzma2Options != null ? lzma2Options : new 
LZMA2Options();
+            return this;
+        }
+
+    }
+
+    /**
+     * Constructs a new builder of {@link XZCompressorOutputStream}.
+     *
+     * @return a new builder of {@link XZCompressorOutputStream}.
+     * @since 1.28.0
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @SuppressWarnings("resource") // Caller closes
+    private XZCompressorOutputStream(final Builder builder) throws IOException 
{
+        super(new XZOutputStream(builder.getOutputStream(), 
builder.lzma2Options));
+        }
+
     /**
      * Creates a new XZ compressor using the default LZMA2 options. This is 
equivalent to {@code XZCompressorOutputStream(outputStream, 6)}.
      *
      * @param outputStream the stream to wrap
      * @throws IOException on error
      */
-    @SuppressWarnings("resource") // Caller closes
     public XZCompressorOutputStream(final OutputStream outputStream) throws 
IOException {
-        super(new XZOutputStream(outputStream, new LZMA2Options()));
+        this(builder().setOutputStream(outputStream));
     }
 
     /**
@@ -62,7 +140,9 @@ public XZCompressorOutputStream(final OutputStream 
outputStream) throws IOExcept
      * @param outputStream the stream to wrap
      * @param preset       the preset
      * @throws IOException on error
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     @SuppressWarnings("resource") // Caller closes
     public XZCompressorOutputStream(final OutputStream outputStream, final int 
preset) throws IOException {
         super(new XZOutputStream(outputStream, new LZMA2Options(preset)));
diff --git 
a/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java
 
b/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java
new file mode 100644
index 000000000..9b24e9710
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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
+ *
+ *   https://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.commons.compress.compressors.xz;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.cartesian.CartesianTest;
+import org.junitpioneer.jupiter.cartesian.CartesianTest.Values;
+import org.tukaani.xz.LZMA2Options;
+import org.tukaani.xz.XZOutputStream;
+
+/**
+ * Tests for class {@link XZCompressorOutputStream} and {@link 
XZCompressorInputStream}.
+ *
+ * @see XZCompressorOutputStream
+ * @see XZCompressorInputStream
+ */
+public class XZCompressorRoundtripTest {
+
+    @TempDir
+    static Path tempDir;
+
+    private void roundtrip(final Path outPath, final LZMA2Options options, 
final boolean decompressConcatenated, final int memoryLimitKiB) throws 
IOException {
+        final String data = "Hello World!";
+        // @formatter:off
+        try (XZCompressorOutputStream out = XZCompressorOutputStream.builder()
+                .setPath(outPath)
+                .setLzma2Options(options)
+                .get()) {
+            out.writeUtf8(data);
+        }
+        try (XZCompressorInputStream out = XZCompressorInputStream.builder()
+                .setPath(outPath)
+                .setDecompressConcatenated(decompressConcatenated)
+                .setMemoryLimitKiB(memoryLimitKiB)
+                .get()) {
+            assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8));
+        }
+        // @formatter:on
+        try (XZCompressorInputStream out = new 
XZCompressorInputStream(Files.newInputStream(outPath))) {
+            assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8));
+        }
+        // deprecated
+        try (XZCompressorOutputStream out = new XZCompressorOutputStream(new 
XZOutputStream(Files.newOutputStream(outPath), options))) {
+            out.writeUtf8(data);
+        }
+    }
+
+    @Test
+    public void testBuilderOptionsAll() throws IOException {
+        final int dictSize = LZMA2Options.DICT_SIZE_MIN;
+        final int lc = LZMA2Options.LC_LP_MAX - 4;
+        final int lp = LZMA2Options.LC_LP_MAX - 4;
+        final int pb = LZMA2Options.PB_MAX;
+        final int mode = LZMA2Options.MODE_NORMAL;
+        final int niceLen = LZMA2Options.NICE_LEN_MIN;
+        final int mf = LZMA2Options.MF_BT4;
+        final int depthLimit = 50;
+        roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(dictSize, lc, 
lp, pb, mode, niceLen, mf, depthLimit), false, -1);
+    }
+
+    @CartesianTest
+    public void testBuilderOptions(@Values(ints = { LZMA2Options.PRESET_MAX, 
LZMA2Options.PRESET_MIN, LZMA2Options.PRESET_DEFAULT }) final int preset,
+            @Values(booleans = { false, true }) final boolean 
decompressConcatenated, @Values(ints = { -1, 100_000 }) final int 
memoryLimitKiB)
+            throws IOException {
+        roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(preset), false, 
-1);
+    }
+
+    @CartesianTest
+    public void testBuilderOptionsDefgault(@Values(booleans = { false, true }) 
final boolean decompressConcatenated,
+            @Values(ints = { -1, 100_000 }) final int memoryLimitKiB) throws 
IOException {
+        roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(), 
decompressConcatenated, memoryLimitKiB);
+    }
+
+    @Test
+    public void testBuilderPath() throws IOException {
+        // This test does not use LZMA2Options
+        final String data = "Hello World!";
+        final Path outPath = tempDir.resolve("out.xz");
+        try (XZCompressorOutputStream out = 
XZCompressorOutputStream.builder().setPath(outPath).get()) {
+            out.writeUtf8(data);
+        }
+        // deprecated
+        try (XZCompressorInputStream out = new 
XZCompressorInputStream(Files.newInputStream(outPath), false)) {
+            assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8));
+        }
+        // deprecated
+        try (XZCompressorInputStream out = new 
XZCompressorInputStream(Files.newInputStream(outPath), false, -1)) {
+            assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8));
+        }
+    }
+}

Reply via email to