View Javadoc
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  
21  import java.io.BufferedInputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.channels.Channels;
27  import java.nio.channels.FileChannel;
28  import java.nio.channels.SeekableByteChannel;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.nio.file.StandardOpenOption;
32  import java.util.Enumeration;
33  import java.util.Iterator;
34  
35  import org.apache.commons.compress.archivers.ArchiveEntry;
36  import org.apache.commons.compress.archivers.ArchiveException;
37  import org.apache.commons.compress.archivers.ArchiveInputStream;
38  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
39  import org.apache.commons.compress.archivers.sevenz.SevenZFile;
40  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
41  import org.apache.commons.compress.archivers.tar.TarFile;
42  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
43  import org.apache.commons.compress.archivers.zip.ZipFile;
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.io.output.NullOutputStream;
46  
47  /**
48   * Provides a high level API for expanding archives.
49   *
50   * @since 1.17
51   */
52  public class Expander {
53  
54      @FunctionalInterface
55      private interface ArchiveEntryBiConsumer<T extends ArchiveEntry> {
56          void accept(T entry, OutputStream out) throws IOException;
57      }
58  
59      @FunctionalInterface
60      private interface ArchiveEntrySupplier<T extends ArchiveEntry> {
61          T get() throws IOException;
62      }
63  
64      /**
65       * @param targetDirectory May be null to simulate output to dev/null on Linux and NUL on Windows.
66       */
67      private <T extends ArchiveEntry> void expand(final ArchiveEntrySupplier<T> supplier, final ArchiveEntryBiConsumer<T> writer, final Path targetDirectory)
68              throws IOException {
69          final boolean nullTarget = targetDirectory == null;
70          final Path targetDirPath = nullTarget ? null : targetDirectory.normalize();
71          T nextEntry = supplier.get();
72          while (nextEntry != null) {
73              final Path targetPath = nullTarget ? null : nextEntry.resolveIn(targetDirPath);
74              if (nextEntry.isDirectory()) {
75                  if (!nullTarget && !Files.isDirectory(targetPath) && Files.createDirectories(targetPath) == null) {
76                      throw new IOException("Failed to create directory " + targetPath);
77                  }
78              } else {
79                  final Path parent = nullTarget ? null : targetPath.getParent();
80                  if (!nullTarget && !Files.isDirectory(parent) && Files.createDirectories(parent) == null) {
81                      throw new IOException("Failed to create directory " + parent);
82                  }
83                  if (nullTarget) {
84                      writer.accept(nextEntry, NullOutputStream.INSTANCE);
85                  } else {
86                      try (OutputStream outputStream = Files.newOutputStream(targetPath)) {
87                          writer.accept(nextEntry, outputStream);
88                      }
89                  }
90              }
91              nextEntry = supplier.get();
92          }
93      }
94  
95      /**
96       * Expands {@code archive} into {@code targetDirectory}.
97       *
98       * @param archive         the file to expand
99       * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
100      * @throws IOException if an I/O error occurs
101      */
102     public void expand(final ArchiveInputStream<?> archive, final File targetDirectory) throws IOException {
103         expand(archive, toPath(targetDirectory));
104     }
105 
106     /**
107      * Expands {@code archive} into {@code targetDirectory}.
108      *
109      * @param archive         the file to expand
110      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
111      * @throws IOException if an I/O error occurs
112      * @since 1.22
113      */
114     public void expand(final ArchiveInputStream<?> archive, final Path targetDirectory) throws IOException {
115         expand(() -> {
116             ArchiveEntry next = archive.getNextEntry();
117             while (next != null && !archive.canReadEntryData(next)) {
118                 next = archive.getNextEntry();
119             }
120             return next;
121         }, (entry, out) -> IOUtils.copy(archive, out), targetDirectory);
122     }
123 
124     /**
125      * Expands {@code archive} into {@code targetDirectory}.
126      *
127      * <p>
128      * Tries to auto-detect the archive's format.
129      * </p>
130      *
131      * @param archive         the file to expand
132      * @param targetDirectory the target directory
133      * @throws IOException      if an I/O error occurs
134      * @throws ArchiveException if the archive cannot be read for other reasons
135      */
136     public void expand(final File archive, final File targetDirectory) throws IOException, ArchiveException {
137         expand(archive.toPath(), toPath(targetDirectory));
138     }
139 
140     /**
141      * Expands {@code archive} into {@code targetDirectory}.
142      *
143      * <p>
144      * Tries to auto-detect the archive's format.
145      * </p>
146      *
147      * <p>
148      * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
149      * {@link #expand(InputStream,File,CloseableConsumer)} instead.
150      * </p>
151      *
152      * @param archive         the file to expand
153      * @param targetDirectory the target directory
154      * @throws IOException      if an I/O error occurs
155      * @throws ArchiveException if the archive cannot be read for other reasons
156      * @deprecated this method leaks resources
157      */
158     @Deprecated
159     public void expand(final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
160         expand(archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
161     }
162 
163     /**
164      * Expands {@code archive} into {@code targetDirectory}.
165      *
166      * <p>
167      * Tries to auto-detect the archive's format.
168      * </p>
169      *
170      * <p>
171      * 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
172      * closing the stream itself. The caller is informed about the wrapper object via the {@code
173      * closeableConsumer} callback as soon as it is no longer needed by this class.
174      * </p>
175      *
176      * @param archive           the file to expand
177      * @param targetDirectory   the target directory
178      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
179      * @throws IOException      if an I/O error occurs
180      * @throws ArchiveException if the archive cannot be read for other reasons
181      * @since 1.19
182      */
183     public void expand(final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer) throws IOException, ArchiveException {
184         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
185             expand(c.track(ArchiveStreamFactory.DEFAULT.createArchiveInputStream(archive)), targetDirectory);
186         }
187     }
188 
189     /**
190      * Expands {@code archive} into {@code targetDirectory}.
191      *
192      * <p>
193      * Tries to auto-detect the archive's format.
194      * </p>
195      *
196      * @param archive         the file to expand
197      * @param targetDirectory the target directory
198      * @throws IOException      if an I/O error occurs
199      * @throws ArchiveException if the archive cannot be read for other reasons
200      * @since 1.22
201      */
202     public void expand(final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
203         try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
204             expand(ArchiveStreamFactory.detect(inputStream), archive, targetDirectory);
205         }
206     }
207 
208     /**
209      * Expands {@code archive} into {@code targetDirectory}.
210      *
211      * @param archive         the file to expand
212      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
213      * @throws IOException if an I/O error occurs
214      */
215     public void expand(final SevenZFile archive, final File targetDirectory) throws IOException {
216         expand(archive, toPath(targetDirectory));
217     }
218 
219     /**
220      * Expands {@code archive} into {@code targetDirectory}.
221      *
222      * @param archive         the file to expand
223      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
224      * @throws IOException if an I/O error occurs
225      * @since 1.22
226      */
227     public void expand(final SevenZFile archive, final Path targetDirectory) throws IOException {
228         expand(archive::getNextEntry, (entry, out) -> {
229             final byte[] buffer = new byte[8192];
230             int n;
231             while (-1 != (n = archive.read(buffer))) {
232                 if (out != null) {
233                     out.write(buffer, 0, n);
234                 }
235             }
236         }, targetDirectory);
237     }
238 
239     /**
240      * Expands {@code archive} into {@code targetDirectory}.
241      *
242      * @param archive         the file to expand
243      * @param targetDirectory the target directory
244      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
245      * @throws IOException      if an I/O error occurs
246      * @throws ArchiveException if the archive cannot be read for other reasons
247      */
248     public void expand(final String format, final File archive, final File targetDirectory) throws IOException, ArchiveException {
249         expand(format, archive.toPath(), toPath(targetDirectory));
250     }
251 
252     /**
253      * Expands {@code archive} into {@code targetDirectory}.
254      *
255      * <p>
256      * This method creates a wrapper around the archive stream which is never closed and thus leaks resources, please use
257      * {@link #expand(String,InputStream,File,CloseableConsumer)} instead.
258      * </p>
259      *
260      * @param archive         the file to expand
261      * @param targetDirectory the target directory
262      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
263      * @throws IOException      if an I/O error occurs
264      * @throws ArchiveException if the archive cannot be read for other reasons
265      * @deprecated this method leaks resources
266      */
267     @Deprecated
268     public void expand(final String format, final InputStream archive, final File targetDirectory) throws IOException, ArchiveException {
269         expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
270     }
271 
272     /**
273      * Expands {@code archive} into {@code targetDirectory}.
274      *
275      * <p>
276      * 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
277      * closing the stream itself. The caller is informed about the wrapper object via the {@code
278      * closeableConsumer} callback as soon as it is no longer needed by this class.
279      * </p>
280      *
281      * @param archive           the file to expand
282      * @param targetDirectory   the target directory
283      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
284      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
285      * @throws IOException      if an I/O error occurs
286      * @throws ArchiveException if the archive cannot be read for other reasons
287      * @since 1.19
288      */
289     public void expand(final String format, final InputStream archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
290             throws IOException, ArchiveException {
291         expand(format, archive, toPath(targetDirectory), closeableConsumer);
292     }
293 
294     /**
295      * Expands {@code archive} into {@code targetDirectory}.
296      *
297      * <p>
298      * 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
299      * closing the stream itself. The caller is informed about the wrapper object via the {@code
300      * closeableConsumer} callback as soon as it is no longer needed by this class.
301      * </p>
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      * @param closeableConsumer is informed about the stream wrapped around the passed in stream
307      * @throws IOException      if an I/O error occurs
308      * @throws ArchiveException if the archive cannot be read for other reasons
309      * @since 1.22
310      */
311     public void expand(final String format, final InputStream archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
312             throws IOException, ArchiveException {
313         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
314             final ArchiveInputStream<?> archiveInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream(format, archive);
315             expand(c.track(archiveInputStream), targetDirectory);
316         }
317     }
318 
319     /**
320      * Expands {@code archive} into {@code targetDirectory}.
321      *
322      * @param archive         the file to expand
323      * @param targetDirectory the target directory
324      * @param format          the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
325      * @throws IOException      if an I/O error occurs
326      * @throws ArchiveException if the archive cannot be read for other reasons
327      * @since 1.22
328      */
329     public void expand(final String format, final Path archive, final Path targetDirectory) throws IOException, ArchiveException {
330         if (prefersSeekableByteChannel(format)) {
331             try (SeekableByteChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
332                 expand(format, channel, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
333             }
334             return;
335         }
336         try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(archive))) {
337             expand(format, inputStream, targetDirectory, CloseableConsumer.CLOSING_CONSUMER);
338         }
339     }
340 
341     /**
342      * Expands {@code archive} into {@code targetDirectory}.
343      *
344      * <p>
345      * This method creates a wrapper around the archive channel which is never closed and thus leaks resources, please use
346      * {@link #expand(String,SeekableByteChannel,File,CloseableConsumer)} instead.
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      * @throws IOException      if an I/O error occurs
353      * @throws ArchiveException if the archive cannot be read for other reasons
354      * @deprecated this method leaks resources
355      */
356     @Deprecated
357     public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory) throws IOException, ArchiveException {
358         expand(format, archive, targetDirectory, CloseableConsumer.NULL_CONSUMER);
359     }
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.19
377      */
378     public void expand(final String format, final SeekableByteChannel archive, final File targetDirectory, final CloseableConsumer closeableConsumer)
379             throws IOException, ArchiveException {
380         expand(format, archive, toPath(targetDirectory), closeableConsumer);
381     }
382 
383     /**
384      * Expands {@code archive} into {@code targetDirectory}.
385      *
386      * <p>
387      * 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
388      * closing the channel itself. The caller is informed about the wrapper object via the {@code
389      * closeableConsumer} callback as soon as it is no longer needed by this class.
390      * </p>
391      *
392      * @param archive           the file to expand
393      * @param targetDirectory   the target directory
394      * @param format            the archive format. This uses the same format as accepted by {@link ArchiveStreamFactory}.
395      * @param closeableConsumer is informed about the stream wrapped around the passed in channel
396      * @throws IOException      if an I/O error occurs
397      * @throws ArchiveException if the archive cannot be read for other reasons
398      * @since 1.22
399      */
400     public void expand(final String format, final SeekableByteChannel archive, final Path targetDirectory, final CloseableConsumer closeableConsumer)
401             throws IOException, ArchiveException {
402         try (CloseableConsumerAdapter c = new CloseableConsumerAdapter(closeableConsumer)) {
403             if (!prefersSeekableByteChannel(format)) {
404                 expand(format, c.track(Channels.newInputStream(archive)), targetDirectory, CloseableConsumer.NULL_CONSUMER);
405             } else if (ArchiveStreamFactory.TAR.equalsIgnoreCase(format)) {
406                 expand(c.track(new TarFile(archive)), targetDirectory);
407             } else if (ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)) {
408                 expand(c.track(ZipFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
409             } else if (ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format)) {
410                 expand(c.track(SevenZFile.builder().setSeekableByteChannel(archive).get()), targetDirectory);
411             } else {
412                 // never reached as prefersSeekableByteChannel only returns true for TAR, ZIP and 7z
413                 throw new ArchiveException("Don't know how to handle format " + format);
414             }
415         }
416     }
417 
418     /**
419      * Expands {@code archive} into {@code targetDirectory}.
420      *
421      * @param archive         the file to expand
422      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
423      * @throws IOException if an I/O error occurs
424      * @since 1.21
425      */
426     public void expand(final TarFile archive, final File targetDirectory) throws IOException {
427         expand(archive, toPath(targetDirectory));
428     }
429 
430     /**
431      * Expands {@code archive} into {@code targetDirectory}.
432      *
433      * @param archive         the file to expand
434      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
435      * @throws IOException if an I/O error occurs
436      * @since 1.22
437      */
438     public void expand(final TarFile archive, final Path targetDirectory) throws IOException {
439         final Iterator<TarArchiveEntry> entryIterator = archive.getEntries().iterator();
440         expand(() -> entryIterator.hasNext() ? entryIterator.next() : null, (entry, out) -> {
441             try (InputStream in = archive.getInputStream(entry)) {
442                 IOUtils.copy(in, out);
443             }
444         }, targetDirectory);
445     }
446 
447     /**
448      * Expands {@code archive} into {@code targetDirectory}.
449      *
450      * @param archive         the file to expand
451      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
452      * @throws IOException if an I/O error occurs
453      */
454     public void expand(final ZipFile archive, final File targetDirectory) throws IOException {
455         expand(archive, toPath(targetDirectory));
456     }
457 
458     /**
459      * Expands {@code archive} into {@code targetDirectory}.
460      *
461      * @param archive         the file to expand
462      * @param targetDirectory the target directory, may be null to simulate output to dev/null on Linux and NUL on Windows.
463      * @throws IOException if an I/O error occurs
464      * @since 1.22
465      */
466     public void expand(final ZipFile archive, final Path targetDirectory) throws IOException {
467         final Enumeration<ZipArchiveEntry> entries = archive.getEntries();
468         expand(() -> {
469             ZipArchiveEntry next = entries.hasMoreElements() ? entries.nextElement() : null;
470             while (next != null && !archive.canReadEntryData(next)) {
471                 next = entries.hasMoreElements() ? entries.nextElement() : null;
472             }
473             return next;
474         }, (entry, out) -> {
475             try (InputStream in = archive.getInputStream(entry)) {
476                 IOUtils.copy(in, out);
477             }
478         }, targetDirectory);
479     }
480 
481     private boolean prefersSeekableByteChannel(final String format) {
482         return ArchiveStreamFactory.TAR.equalsIgnoreCase(format) || ArchiveStreamFactory.ZIP.equalsIgnoreCase(format)
483                 || ArchiveStreamFactory.SEVEN_Z.equalsIgnoreCase(format);
484     }
485 
486     private Path toPath(final File targetDirectory) {
487         return targetDirectory != null ? targetDirectory.toPath() : null;
488     }
489 
490 }