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.changes;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.Enumeration;
24  import java.util.Iterator;
25  import java.util.LinkedHashSet;
26  import java.util.Set;
27  
28  import org.apache.commons.compress.archivers.ArchiveEntry;
29  import org.apache.commons.compress.archivers.ArchiveInputStream;
30  import org.apache.commons.compress.archivers.ArchiveOutputStream;
31  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
32  import org.apache.commons.compress.archivers.zip.ZipFile;
33  import org.apache.commons.compress.changes.Change.ChangeType;
34  
35  /**
36   * 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
37   * changes, a new Performer must be created.
38   *
39   * @param <I> The {@link ArchiveInputStream} type.
40   * @param <O> The {@link ArchiveOutputStream} type.
41   * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
42   * @ThreadSafe
43   * @Immutable
44   */
45  public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {
46  
47      /**
48       * Abstracts getting entries and streams for archive entries.
49       *
50       * <p>
51       * 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
52       * exceptions thrown in ArchiveInputStream#getNextEntry.
53       * </p>
54       */
55      private interface ArchiveEntryIterator<E extends ArchiveEntry> {
56  
57          InputStream getInputStream() throws IOException;
58  
59          boolean hasNext() throws IOException;
60  
61          E next();
62      }
63  
64      private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {
65  
66          private final ArchiveInputStream<E> inputStream;
67          private E next;
68  
69          ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
70              this.inputStream = inputStream;
71          }
72  
73          @Override
74          public InputStream getInputStream() {
75              return inputStream;
76          }
77  
78          @Override
79          public boolean hasNext() throws IOException {
80              return (next = inputStream.getNextEntry()) != null;
81          }
82  
83          @Override
84          public E next() {
85              return next;
86          }
87      }
88  
89      private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {
90  
91          private final ZipFile zipFile;
92          private final Enumeration<ZipArchiveEntry> nestedEnumeration;
93          private ZipArchiveEntry currentEntry;
94  
95          ZipFileIterator(final ZipFile zipFile) {
96              this.zipFile = zipFile;
97              this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
98          }
99  
100         @Override
101         public InputStream getInputStream() throws IOException {
102             return zipFile.getInputStream(currentEntry);
103         }
104 
105         @Override
106         public boolean hasNext() {
107             return nestedEnumeration.hasMoreElements();
108         }
109 
110         @Override
111         public ZipArchiveEntry next() {
112             return currentEntry = nestedEnumeration.nextElement();
113         }
114     }
115 
116     private final Set<Change<E>> changes;
117 
118     /**
119      * Constructs a ChangeSetPerformer with the changes from this ChangeSet
120      *
121      * @param changeSet the ChangeSet which operations are used for performing
122      */
123     public ChangeSetPerformer(final ChangeSet<E> changeSet) {
124         this.changes = changeSet.getChanges();
125     }
126 
127     /**
128      * Copies the ArchiveEntry to the Output stream
129      *
130      * @param inputStream  the stream to read the data from
131      * @param outputStream the stream to write the data to
132      * @param archiveEntry the entry to write
133      * @throws IOException if data cannot be read or written
134      */
135     private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
136         outputStream.putArchiveEntry(archiveEntry);
137         org.apache.commons.io.IOUtils.copy(inputStream, outputStream);
138         outputStream.closeArchiveEntry();
139     }
140 
141     /**
142      * 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
143      * same set.
144      *
145      * @param entry the entry to check
146      * @return true, if this entry has a deletion change later, false otherwise
147      */
148     private boolean isDeletedLater(final Set<Change<E>> workingSet, final E entry) {
149         final String source = entry.getName();
150 
151         if (!workingSet.isEmpty()) {
152             for (final Change<E> change : workingSet) {
153                 final ChangeType type = change.getType();
154                 final String target = change.getTargetFileName();
155                 if (type == ChangeType.DELETE && source.equals(target)) {
156                     return true;
157                 }
158 
159                 if (type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
160                     return true;
161                 }
162             }
163         }
164         return false;
165     }
166 
167     /**
168      * Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
169      *
170      * This method finishes the stream, no other entries should be added after that.
171      *
172      * @param entryIterator the entries to perform the changes on
173      * @param outputStream  the resulting OutputStream with all modifications
174      * @throws IOException if a read/write error occurs
175      * @return the results of this operation
176      */
177     private ChangeSetResults perform(final ArchiveEntryIterator<E> entryIterator, final O outputStream) throws IOException {
178         final ChangeSetResults results = new ChangeSetResults();
179 
180         final Set<Change<E>> workingSet = new LinkedHashSet<>(changes);
181 
182         for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
183             final Change<E> change = it.next();
184 
185             if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
186                 @SuppressWarnings("resource") // InputStream not allocated here
187                 final InputStream inputStream = change.getInputStream();
188                 copyStream(inputStream, outputStream, change.getEntry());
189                 it.remove();
190                 results.addedFromChangeSet(change.getEntry().getName());
191             }
192         }
193 
194         while (entryIterator.hasNext()) {
195             final E entry = entryIterator.next();
196             boolean copy = true;
197 
198             for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
199                 final Change<E> change = it.next();
200 
201                 final ChangeType type = change.getType();
202                 final String name = entry.getName();
203                 if (type == ChangeType.DELETE && name != null) {
204                     if (name.equals(change.getTargetFileName())) {
205                         copy = false;
206                         it.remove();
207                         results.deleted(name);
208                         break;
209                     }
210                 } else if (type == ChangeType.DELETE_DIR && name != null) {
211                     // don't combine ifs to make future extensions more easy
212                     if (name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
213                         copy = false;
214                         results.deleted(name);
215                         break;
216                     }
217                 }
218             }
219 
220             if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
221                 @SuppressWarnings("resource") // InputStream not allocated here
222                 final InputStream inputStream = entryIterator.getInputStream();
223                 copyStream(inputStream, outputStream, entry);
224                 results.addedFromStream(entry.getName());
225             }
226         }
227 
228         // Adds files which hasn't been added from the original and do not have replace mode on
229         for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
230             final Change<E> change = it.next();
231 
232             if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
233                 @SuppressWarnings("resource")
234                 final InputStream input = change.getInputStream();
235                 copyStream(input, outputStream, change.getEntry());
236                 it.remove();
237                 results.addedFromChangeSet(change.getEntry().getName());
238             }
239         }
240         outputStream.finish();
241         return results;
242     }
243 
244     /**
245      * 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.
246      *
247      * This method finishes the stream, no other entries should be added after that.
248      *
249      * @param inputStream  the InputStream to perform the changes on
250      * @param outputStream the resulting OutputStream with all modifications
251      * @throws IOException if a read/write error occurs
252      * @return the results of this operation
253      */
254     public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
255         return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
256     }
257 
258     /**
259      * 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.
260      *
261      * This method finishes the stream, no other entries should be added after that.
262      *
263      * @param zipFile      the ZipFile to perform the changes on
264      * @param outputStream the resulting OutputStream with all modifications
265      * @throws IOException if a read/write error occurs
266      * @return the results of this operation
267      * @since 1.5
268      */
269     public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
270         @SuppressWarnings("unchecked")
271         final ArchiveEntryIterator<E> entryIterator = (ArchiveEntryIterator<E>) new ZipFileIterator(zipFile);
272         return perform(entryIterator, outputStream);
273     }
274 }