ChangeSetPerformer.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.changes;

  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.util.Enumeration;
  23. import java.util.Iterator;
  24. import java.util.LinkedHashSet;
  25. import java.util.Set;

  26. import org.apache.commons.compress.archivers.ArchiveEntry;
  27. import org.apache.commons.compress.archivers.ArchiveInputStream;
  28. import org.apache.commons.compress.archivers.ArchiveOutputStream;
  29. import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
  30. import org.apache.commons.compress.archivers.zip.ZipFile;
  31. import org.apache.commons.compress.changes.Change.ChangeType;

  32. /**
  33.  * Performs ChangeSet operations on a stream. This class is thread safe and can be used multiple times. It operates on a copy of the ChangeSet. If the ChangeSet
  34.  * changes, a new Performer must be created.
  35.  *
  36.  * @param <I> The {@link ArchiveInputStream} type.
  37.  * @param <O> The {@link ArchiveOutputStream} type.
  38.  * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
  39.  * @ThreadSafe
  40.  * @Immutable
  41.  */
  42. public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {

  43.     /**
  44.      * Abstracts getting entries and streams for archive entries.
  45.      *
  46.      * <p>
  47.      * Iterator#hasNext is not allowed to throw exceptions that's why we can't use Iterator&lt;ArchiveEntry&gt; directly - otherwise we'd need to convert
  48.      * exceptions thrown in ArchiveInputStream#getNextEntry.
  49.      * </p>
  50.      */
  51.     private interface ArchiveEntryIterator<E extends ArchiveEntry> {

  52.         InputStream getInputStream() throws IOException;

  53.         boolean hasNext() throws IOException;

  54.         E next();
  55.     }

  56.     private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {

  57.         private final ArchiveInputStream<E> inputStream;
  58.         private E next;

  59.         ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
  60.             this.inputStream = inputStream;
  61.         }

  62.         @Override
  63.         public InputStream getInputStream() {
  64.             return inputStream;
  65.         }

  66.         @Override
  67.         public boolean hasNext() throws IOException {
  68.             return (next = inputStream.getNextEntry()) != null;
  69.         }

  70.         @Override
  71.         public E next() {
  72.             return next;
  73.         }
  74.     }

  75.     private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {

  76.         private final ZipFile zipFile;
  77.         private final Enumeration<ZipArchiveEntry> nestedEnumeration;
  78.         private ZipArchiveEntry currentEntry;

  79.         ZipFileIterator(final ZipFile zipFile) {
  80.             this.zipFile = zipFile;
  81.             this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
  82.         }

  83.         @Override
  84.         public InputStream getInputStream() throws IOException {
  85.             return zipFile.getInputStream(currentEntry);
  86.         }

  87.         @Override
  88.         public boolean hasNext() {
  89.             return nestedEnumeration.hasMoreElements();
  90.         }

  91.         @Override
  92.         public ZipArchiveEntry next() {
  93.             return currentEntry = nestedEnumeration.nextElement();
  94.         }
  95.     }

  96.     private final Set<Change<E>> changes;

  97.     /**
  98.      * Constructs a ChangeSetPerformer with the changes from this ChangeSet
  99.      *
  100.      * @param changeSet the ChangeSet which operations are used for performing
  101.      */
  102.     public ChangeSetPerformer(final ChangeSet<E> changeSet) {
  103.         this.changes = changeSet.getChanges();
  104.     }

  105.     /**
  106.      * Copies the ArchiveEntry to the Output stream
  107.      *
  108.      * @param inputStream  the stream to read the data from
  109.      * @param outputStream the stream to write the data to
  110.      * @param archiveEntry the entry to write
  111.      * @throws IOException if data cannot be read or written
  112.      */
  113.     private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
  114.         outputStream.putArchiveEntry(archiveEntry);
  115.         org.apache.commons.io.IOUtils.copy(inputStream, outputStream);
  116.         outputStream.closeArchiveEntry();
  117.     }

  118.     /**
  119.      * Checks if an ArchiveEntry is deleted later in the ChangeSet. This is necessary if a file is added with this ChangeSet, but later became deleted in the
  120.      * same set.
  121.      *
  122.      * @param entry the entry to check
  123.      * @return true, if this entry has a deletion change later, false otherwise
  124.      */
  125.     private boolean isDeletedLater(final Set<Change<E>> workingSet, final E entry) {
  126.         final String source = entry.getName();

  127.         if (!workingSet.isEmpty()) {
  128.             for (final Change<E> change : workingSet) {
  129.                 final ChangeType type = change.getType();
  130.                 final String target = change.getTargetFileName();
  131.                 if (type == ChangeType.DELETE && source.equals(target)) {
  132.                     return true;
  133.                 }

  134.                 if (type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
  135.                     return true;
  136.                 }
  137.             }
  138.         }
  139.         return false;
  140.     }

  141.     /**
  142.      * Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
  143.      *
  144.      * This method finishes the stream, no other entries should be added after that.
  145.      *
  146.      * @param entryIterator the entries to perform the changes on
  147.      * @param outputStream  the resulting OutputStream with all modifications
  148.      * @throws IOException if a read/write error occurs
  149.      * @return the results of this operation
  150.      */
  151.     private ChangeSetResults perform(final ArchiveEntryIterator<E> entryIterator, final O outputStream) throws IOException {
  152.         final ChangeSetResults results = new ChangeSetResults();

  153.         final Set<Change<E>> workingSet = new LinkedHashSet<>(changes);

  154.         for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
  155.             final Change<E> change = it.next();

  156.             if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
  157.                 @SuppressWarnings("resource") // InputStream not allocated here
  158.                 final InputStream inputStream = change.getInputStream();
  159.                 copyStream(inputStream, outputStream, change.getEntry());
  160.                 it.remove();
  161.                 results.addedFromChangeSet(change.getEntry().getName());
  162.             }
  163.         }

  164.         while (entryIterator.hasNext()) {
  165.             final E entry = entryIterator.next();
  166.             boolean copy = true;

  167.             for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
  168.                 final Change<E> change = it.next();

  169.                 final ChangeType type = change.getType();
  170.                 final String name = entry.getName();
  171.                 if (type == ChangeType.DELETE && name != null) {
  172.                     if (name.equals(change.getTargetFileName())) {
  173.                         copy = false;
  174.                         it.remove();
  175.                         results.deleted(name);
  176.                         break;
  177.                     }
  178.                 } else if (type == ChangeType.DELETE_DIR && name != null) {
  179.                     // don't combine ifs to make future extensions more easy
  180.                     if (name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
  181.                         copy = false;
  182.                         results.deleted(name);
  183.                         break;
  184.                     }
  185.                 }
  186.             }

  187.             if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
  188.                 @SuppressWarnings("resource") // InputStream not allocated here
  189.                 final InputStream inputStream = entryIterator.getInputStream();
  190.                 copyStream(inputStream, outputStream, entry);
  191.                 results.addedFromStream(entry.getName());
  192.             }
  193.         }

  194.         // Adds files which hasn't been added from the original and do not have replace mode on
  195.         for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
  196.             final Change<E> change = it.next();

  197.             if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
  198.                 @SuppressWarnings("resource")
  199.                 final InputStream input = change.getInputStream();
  200.                 copyStream(input, outputStream, change.getEntry());
  201.                 it.remove();
  202.                 results.addedFromChangeSet(change.getEntry().getName());
  203.             }
  204.         }
  205.         outputStream.finish();
  206.         return results;
  207.     }

  208.     /**
  209.      * Performs all changes collected in this ChangeSet on the input stream and streams the result to the output stream. Perform may be called more than once.
  210.      *
  211.      * This method finishes the stream, no other entries should be added after that.
  212.      *
  213.      * @param inputStream  the InputStream to perform the changes on
  214.      * @param outputStream the resulting OutputStream with all modifications
  215.      * @throws IOException if a read/write error occurs
  216.      * @return the results of this operation
  217.      */
  218.     public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
  219.         return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
  220.     }

  221.     /**
  222.      * Performs all changes collected in this ChangeSet on the ZipFile and streams the result to the output stream. Perform may be called more than once.
  223.      *
  224.      * This method finishes the stream, no other entries should be added after that.
  225.      *
  226.      * @param zipFile      the ZipFile to perform the changes on
  227.      * @param outputStream the resulting OutputStream with all modifications
  228.      * @throws IOException if a read/write error occurs
  229.      * @return the results of this operation
  230.      * @since 1.5
  231.      */
  232.     public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
  233.         @SuppressWarnings("unchecked")
  234.         final ArchiveEntryIterator<E> entryIterator = (ArchiveEntryIterator<E>) new ZipFileIterator(zipFile);
  235.         return perform(entryIterator, outputStream);
  236.     }
  237. }