Expander.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.archivers.examples;

  20. import java.io.BufferedInputStream;
  21. import java.io.File;
  22. import java.io.IOException;
  23. import java.io.InputStream;
  24. import java.io.OutputStream;
  25. import java.nio.channels.Channels;
  26. import java.nio.channels.FileChannel;
  27. import java.nio.channels.SeekableByteChannel;
  28. import java.nio.file.Files;
  29. import java.nio.file.Path;
  30. import java.nio.file.StandardOpenOption;
  31. import java.util.Enumeration;
  32. import java.util.Iterator;

  33. import org.apache.commons.compress.archivers.ArchiveEntry;
  34. import org.apache.commons.compress.archivers.ArchiveException;
  35. import org.apache.commons.compress.archivers.ArchiveInputStream;
  36. import org.apache.commons.compress.archivers.ArchiveStreamFactory;
  37. import org.apache.commons.compress.archivers.sevenz.SevenZFile;
  38. import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
  39. import org.apache.commons.compress.archivers.tar.TarFile;
  40. import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
  41. import org.apache.commons.compress.archivers.zip.ZipFile;
  42. import org.apache.commons.io.IOUtils;
  43. import org.apache.commons.io.output.NullOutputStream;

  44. /**
  45.  * Provides a high level API for expanding archives.
  46.  *
  47.  * @since 1.17
  48.  */
  49. public class Expander {

  50.     @FunctionalInterface
  51.     private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> {
  52.         void accept(T entry, OutputStream out) throws IOException;
  53.     }

  54.     @FunctionalInterface
  55.     private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
  56.         T get() throws IOException;
  57.     }

  58.     /**
  59.      * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows.
  60.      */
  61.     private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory)
  62.             throws IOException {
  63.         final boolean nullTarget = targetDirectory == null;
  64.         final Path targetDirPath = nullTarget ? null : targetDirectory.normalize();
  65.         T nextEntry = supplier.get();
  66.         while (nextEntry != null) {
  67.             final Path targetPath = nullTarget ? null : nextEntry.resolveIn(targetDirPath);
  68.             if (nextEntry.isDirectory()) {
  69.                 if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) {
  70.                     throw new IOException("Failed to create directory " + targetPath);
  71.                 }
  72.             } else {
  73.                 final Path parent = nullTarget ? null : targetPath.getParent();
  74.                 if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) {
  75.                     throw new IOException("Failed to create directory " + parent);
  76.                 }
  77.                 if (nullTarget) {
  78.                     writer.accept(nextEntry, NullOutputStream.INSTANCE);
  79.                 } else {
  80.                     try (OutputStream outputStream = Files.newOutputStream(targetPath)) {
  81.                         writer.accept(nextEntry, outputStream);
  82.                     }
  83.                 }
  84.             }
  85.             nextEntry = supplier.get();
  86.         }
  87.     }

  88.     /**
  89.      * Expands {@code archive} into {@code targetDirectory}.
  90.      *
  91.      * @param archive         the file to expand
  92.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  93.      * @throws IOException if an I/O error occurs
  94.      */
  95.     public void expand(final ArchiveInputStream<?> archive, final File targetDirectory) throws IOException {
  96.         expand(archive, toPath(targetDirectory));
  97.     }

  98.     /**
  99.      * Expands {@code archive} into {@code targetDirectory}.
  100.      *
  101.      * @param archive         the file to expand
  102.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  103.      * @throws IOException if an I/O error occurs
  104.      * @since 1.22
  105.      */
  106.     public void expand(final ArchiveInputStream<?> archive, final Path targetDirectory) throws IOException {
  107.         expand(() -> {
  108.             ArchiveEntry next = archive.getNextEntry();
  109.             while (next != null && !archive.canReadEntryData(next)) {
  110.                 next = archive.getNextEntry();
  111.             }
  112.             return next;
  113.         }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
  114.     }

  115.     /**
  116.      * Expands {@code archive} into {@code targetDirectory}.
  117.      *
  118.      * <p>
  119.      * Tries to auto-detect the archive's format.
  120.      * </p>
  121.      *
  122.      * @param archive         the file to expand
  123.      * @param targetDirectory the target directory
  124.      * @throws IOException      if an I/O error occurs
  125.      * @throws ArchiveException if the archive cannot be read for other reasons
  126.      */
  127.     public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
  128.         expand(archive.toPath(), toPath(targetDirectory));
  129.     }

  130.     /**
  131.      * Expands {@code archive} into {@code targetDirectory}.
  132.      *
  133.      * <p>
  134.      * Tries to auto-detect the archive's format.
  135.      * </p>
  136.      *
  137.      * <p>
  138.      * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
  139.      * {@link #expand(InputStream,File,CloseableConsumer)} instead.
  140.      * </p>
  141.      *
  142.      * @param archive         the file to expand
  143.      * @param targetDirectory the target directory
  144.      * @throws IOException      if an I/O error occurs
  145.      * @throws ArchiveException if the archive cannot be read for other reasons
  146.      * @deprecated this method leaks resources
  147.      */
  148.     @Deprecated
  149.     public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
  150.         expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
  151.     }

  152.     /**
  153.      * Expands {@code archive} into {@code targetDirectory}.
  154.      *
  155.      * <p>
  156.      * Tries to auto-detect the archive's format.
  157.      * </p>
  158.      *
  159.      * <p>
  160.      * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing it - probably at the same time as
  161.      * closing the stream itself. The caller is informed about the wrapper object via the {@code
  162.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  163.      * </p>
  164.      *
  165.      * @param archive           the file to expand
  166.      * @param targetDirectory   the target directory
  167.      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
  168.      * @throws IOException      if an I/O error occurs
  169.      * @throws ArchiveException if the archive cannot be read for other reasons
  170.      * @since 1.19
  171.      */
  172.     public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
  173.         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
  174.             expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), targetDirectory);
  175.         }
  176.     }

  177.     /**
  178.      * Expands {@code archive} into {@code targetDirectory}.
  179.      *
  180.      * <p>
  181.      * Tries to auto-detect the archive's format.
  182.      * </p>
  183.      *
  184.      * @param archive         the file to expand
  185.      * @param targetDirectory the target directory
  186.      * @throws IOException      if an I/O error occurs
  187.      * @throws ArchiveException if the archive cannot be read for other reasons
  188.      * @since 1.22
  189.      */
  190.     public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
  191.         try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
  192.             expand(ArchiveStreamFactory.detect(inputStream), archive, targetDirectory);
  193.         }
  194.     }

  195.     /**
  196.      * Expands {@code archive} into {@code targetDirectory}.
  197.      *
  198.      * @param archive         the file to expand
  199.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  200.      * @throws IOException if an I/O error occurs
  201.      */
  202.     public void expand(final SevenZFile archive, final File targetDirectory) throws IOException {
  203.         expand(archive, toPath(targetDirectory));
  204.     }

  205.     /**
  206.      * Expands {@code archive} into {@code targetDirectory}.
  207.      *
  208.      * @param archive         the file to expand
  209.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  210.      * @throws IOException if an I/O error occurs
  211.      * @since 1.22
  212.      */
  213.     public void expand(final SevenZFile archive, final Path targetDirectory) throws IOException {
  214.         expand(archive::getNextEntry, (entry, out) -> {
  215.             final byte[] buffer = new byte[8192];
  216.             int n;
  217.             while (-1 != (n = archive.read(buffer))) {
  218.                 if (out != null) {
  219.                     out.write(buffer, 0, n);
  220.                 }
  221.             }
  222.         }, targetDirectory);
  223.     }

  224.     /**
  225.      * Expands {@code archive} into {@code targetDirectory}.
  226.      *
  227.      * @param archive         the file to expand
  228.      * @param targetDirectory the target directory
  229.      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  230.      * @throws IOException      if an I/O error occurs
  231.      * @throws ArchiveException if the archive cannot be read for other reasons
  232.      */
  233.     public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
  234.         expand(format, archive.toPath(), toPath(targetDirectory));
  235.     }

  236.     /**
  237.      * Expands {@code archive} into {@code targetDirectory}.
  238.      *
  239.      * <p>
  240.      * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
  241.      * {@link #expand(String,InputStream,File,CloseableConsumer)} instead.
  242.      * </p>
  243.      *
  244.      * @param archive         the file to expand
  245.      * @param targetDirectory the target directory
  246.      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  247.      * @throws IOException      if an I/O error occurs
  248.      * @throws ArchiveException if the archive cannot be read for other reasons
  249.      * @deprecated this method leaks resources
  250.      */
  251.     @Deprecated
  252.     public void expand(final String format, final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
  253.         expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
  254.     }

  255.     /**
  256.      * Expands {@code archive} into {@code targetDirectory}.
  257.      *
  258.      * <p>
  259.      * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing it - probably at the same time as
  260.      * closing the stream itself. The caller is informed about the wrapper object via the {@code
  261.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  262.      * </p>
  263.      *
  264.      * @param archive           the file to expand
  265.      * @param targetDirectory   the target directory
  266.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  267.      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
  268.      * @throws IOException      if an I/O error occurs
  269.      * @throws ArchiveException if the archive cannot be read for other reasons
  270.      * @since 1.19
  271.      */
  272.     public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
  273.             throws IOException, ArchiveException {
  274.         expand(format, archive, toPath(targetDirectory), closeableConsumer);
  275.     }

  276.     /**
  277.      * Expands {@code archive} into {@code targetDirectory}.
  278.      *
  279.      * <p>
  280.      * This method creates a wrapper around the archive stream and the caller of this method is responsible for closing it - probably at the same time as
  281.      * closing the stream itself. The caller is informed about the wrapper object via the {@code
  282.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  283.      * </p>
  284.      *
  285.      * @param archive           the file to expand
  286.      * @param targetDirectory   the target directory
  287.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  288.      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
  289.      * @throws IOException      if an I/O error occurs
  290.      * @throws ArchiveException if the archive cannot be read for other reasons
  291.      * @since 1.22
  292.      */
  293.     public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
  294.             throws IOException, ArchiveException {
  295.         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
  296.             final ArchiveInputStream<?> archiveInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive);
  297.             expand(c.track(archiveInputStream), targetDirectory);
  298.         }
  299.     }

  300.     /**
  301.      * Expands {@code archive} into {@code targetDirectory}.
  302.      *
  303.      * @param archive         the file to expand
  304.      * @param targetDirectory the target directory
  305.      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  306.      * @throws IOException      if an I/O error occurs
  307.      * @throws ArchiveException if the archive cannot be read for other reasons
  308.      * @since 1.22
  309.      */
  310.     public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
  311.         if (prefersSeekableByteChannel(format)) {
  312.             try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
  313.                 expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
  314.             }
  315.             return;
  316.         }
  317.         try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
  318.             expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
  319.         }
  320.     }

  321.     /**
  322.      * Expands {@code archive} into {@code targetDirectory}.
  323.      *
  324.      * <p>
  325.      * This method creates a wrapper around the archive channel which is never closed and thus leaks resources, please use
  326.      * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} instead.
  327.      * </p>
  328.      *
  329.      * @param archive         the file to expand
  330.      * @param targetDirectory the target directory
  331.      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  332.      * @throws IOException      if an I/O error occurs
  333.      * @throws ArchiveException if the archive cannot be read for other reasons
  334.      * @deprecated this method leaks resources
  335.      */
  336.     @Deprecated
  337.     public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) throws IOException, ArchiveException {
  338.         expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
  339.     }

  340.     /**
  341.      * Expands {@code archive} into {@code targetDirectory}.
  342.      *
  343.      * <p>
  344.      * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing it - probably at the same time as
  345.      * closing the channel itself. The caller is informed about the wrapper object via the {@code
  346.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  347.      * </p>
  348.      *
  349.      * @param archive           the file to expand
  350.      * @param targetDirectory   the target directory
  351.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  352.      * @param closeableConsumer is informed about the stream wrapped around the passed in channel
  353.      * @throws IOException      if an I/O error occurs
  354.      * @throws ArchiveException if the archive cannot be read for other reasons
  355.      * @since 1.19
  356.      */
  357.     public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
  358.             throws IOException, ArchiveException {
  359.         expand(format, archive, toPath(targetDirectory), closeableConsumer);
  360.     }

  361.     /**
  362.      * Expands {@code archive} into {@code targetDirectory}.
  363.      *
  364.      * <p>
  365.      * This method creates a wrapper around the archive channel and the caller of this method is responsible for closing it - probably at the same time as
  366.      * closing the channel itself. The caller is informed about the wrapper object via the {@code
  367.      * closeableConsumer} callback as soon as it is no longer needed by this class.
  368.      * </p>
  369.      *
  370.      * @param archive           the file to expand
  371.      * @param targetDirectory   the target directory
  372.      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
  373.      * @param closeableConsumer is informed about the stream wrapped around the passed in channel
  374.      * @throws IOException      if an I/O error occurs
  375.      * @throws ArchiveException if the archive cannot be read for other reasons
  376.      * @since 1.22
  377.      */
  378.     public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
  379.             throws IOException, ArchiveException {
  380.         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
  381.             if (!prefersSeekableByteChannel(format)) {
  382.                 expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
  383.             } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
  384.                 expand(c.track(new TarFile(archive)), targetDirectory);
  385.             } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
  386.                 expand(c.track(ZipFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
  387.             } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
  388.                 expand(c.track(SevenZFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
  389.             } else {
  390.                 // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
  391.                 throw new ArchiveException("Don't know how to handle format " + format);
  392.             }
  393.         }
  394.     }

  395.     /**
  396.      * Expands {@code archive} into {@code targetDirectory}.
  397.      *
  398.      * @param archive         the file to expand
  399.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  400.      * @throws IOException if an I/O error occurs
  401.      * @since 1.21
  402.      */
  403.     public void expand(final TarFile archive, final File targetDirectory) throws IOException {
  404.         expand(archive, toPath(targetDirectory));
  405.     }

  406.     /**
  407.      * Expands {@code archive} into {@code targetDirectory}.
  408.      *
  409.      * @param archive         the file to expand
  410.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  411.      * @throws IOException if an I/O error occurs
  412.      * @since 1.22
  413.      */
  414.     public void expand(final TarFile archive, final Path targetDirectory) throws IOException {
  415.         final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
  416.         expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, (entry, out) -> {
  417.             try (InputStream in = archive.getInputStream(entry)) {
  418.                 IOUtils.copy(in, out);
  419.             }
  420.         }, targetDirectory);
  421.     }

  422.     /**
  423.      * Expands {@code archive} into {@code targetDirectory}.
  424.      *
  425.      * @param archive         the file to expand
  426.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  427.      * @throws IOException if an I/O error occurs
  428.      */
  429.     public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
  430.         expand(archive, toPath(targetDirectory));
  431.     }

  432.     /**
  433.      * Expands {@code archive} into {@code targetDirectory}.
  434.      *
  435.      * @param archive         the file to expand
  436.      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
  437.      * @throws IOException if an I/O error occurs
  438.      * @since 1.22
  439.      */
  440.     public void expand(final ZipFile archive, final Path targetDirectory) throws IOException {
  441.         final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
  442.         expand(() -> {
  443.             ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
  444.             while (next != null && !archive.canReadEntryData(next)) {
  445.                 next = entries.hasMoreElements() ? entries.nextElement() : null;
  446.             }
  447.             return next;
  448.         }, (entry, out) -> {
  449.             try (InputStream in = archive.getInputStream(entry)) {
  450.                 IOUtils.copy(in, out);
  451.             }
  452.         }, targetDirectory);
  453.     }

  454.     private boolean prefersSeekableByteChannel(final String format) {
  455.         return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
  456.                 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
  457.     }

  458.     private Path toPath(final File targetDirectory) {
  459.         return targetDirectory != null ? targetDirectory.toPath() : null;
  460.     }

  461. }