001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.changes;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.Enumeration;
024import java.util.Iterator;
025import java.util.LinkedHashSet;
026import java.util.Set;
027
028import org.apache.commons.compress.archivers.ArchiveEntry;
029import org.apache.commons.compress.archivers.ArchiveInputStream;
030import org.apache.commons.compress.archivers.ArchiveOutputStream;
031import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
032import org.apache.commons.compress.archivers.zip.ZipFile;
033import org.apache.commons.compress.changes.Change.ChangeType;
034import org.apache.commons.compress.utils.IOUtils;
035
036/**
037 * 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
038 * changes, a new Performer must be created.
039 *
040 * @param <I> The {@link ArchiveInputStream} type.
041 * @param <O> The {@link ArchiveOutputStream} type.
042 * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
043 * @ThreadSafe
044 * @Immutable
045 */
046public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {
047
048    /**
049     * Abstracts getting entries and streams for archive entries.
050     *
051     * <p>
052     * 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
053     * exceptions thrown in ArchiveInputStream#getNextEntry.
054     * </p>
055     */
056    private interface ArchiveEntryIterator<E extends ArchiveEntry> {
057
058        InputStream getInputStream() throws IOException;
059
060        boolean hasNext() throws IOException;
061
062        E next();
063    }
064
065    private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {
066
067        private final ArchiveInputStream<E> inputStream;
068        private E next;
069
070        ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
071            this.inputStream = inputStream;
072        }
073
074        @Override
075        public InputStream getInputStream() {
076            return inputStream;
077        }
078
079        @Override
080        public boolean hasNext() throws IOException {
081            return (next = inputStream.getNextEntry()) != null;
082        }
083
084        @Override
085        public E next() {
086            return next;
087        }
088    }
089
090    private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {
091
092        private final ZipFile zipFile;
093        private final Enumeration<ZipArchiveEntry> nestedEnumeration;
094        private ZipArchiveEntry currentEntry;
095
096        ZipFileIterator(final ZipFile zipFile) {
097            this.zipFile = zipFile;
098            this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
099        }
100
101        @Override
102        public InputStream getInputStream() throws IOException {
103            return zipFile.getInputStream(currentEntry);
104        }
105
106        @Override
107        public boolean hasNext() {
108            return nestedEnumeration.hasMoreElements();
109        }
110
111        @Override
112        public ZipArchiveEntry next() {
113            return currentEntry = nestedEnumeration.nextElement();
114        }
115    }
116
117    private final Set<Change<E>> changes;
118
119    /**
120     * Constructs a ChangeSetPerformer with the changes from this ChangeSet
121     *
122     * @param changeSet the ChangeSet which operations are used for performing
123     */
124    public ChangeSetPerformer(final ChangeSet<E> changeSet) {
125        this.changes = changeSet.getChanges();
126    }
127
128    /**
129     * Copies the ArchiveEntry to the Output stream
130     *
131     * @param inputStream  the stream to read the data from
132     * @param outputStream the stream to write the data to
133     * @param archiveEntry the entry to write
134     * @throws IOException if data cannot be read or written
135     */
136    private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
137        outputStream.putArchiveEntry(archiveEntry);
138        IOUtils.copy(inputStream, outputStream);
139        outputStream.closeArchiveEntry();
140    }
141
142    /**
143     * 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
144     * same set.
145     *
146     * @param entry the entry to check
147     * @return true, if this entry has a deletion change later, false otherwise
148     */
149    private boolean isDeletedLater(final Set<Change<E>> workingSet, final E entry) {
150        final String source = entry.getName();
151
152        if (!workingSet.isEmpty()) {
153            for (final Change<E> change : workingSet) {
154                final ChangeType type = change.getType();
155                final String target = change.getTargetFileName();
156                if (type == ChangeType.DELETE && source.equals(target)) {
157                    return true;
158                }
159
160                if (type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
161                    return true;
162                }
163            }
164        }
165        return false;
166    }
167
168    /**
169     * Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
170     *
171     * This method finishes the stream, no other entries should be added after that.
172     *
173     * @param entryIterator the entries to perform the changes on
174     * @param outputStream  the resulting OutputStream with all modifications
175     * @throws IOException if a read/write error occurs
176     * @return the results of this operation
177     */
178    private ChangeSetResults perform(final ArchiveEntryIterator<E> entryIterator, final O outputStream) throws IOException {
179        final ChangeSetResults results = new ChangeSetResults();
180
181        final Set<Change<E>> workingSet = new LinkedHashSet<>(changes);
182
183        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
184            final Change<E> change = it.next();
185
186            if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
187                @SuppressWarnings("resource") // InputStream not allocated here
188                final InputStream inputStream = change.getInputStream();
189                copyStream(inputStream, outputStream, change.getEntry());
190                it.remove();
191                results.addedFromChangeSet(change.getEntry().getName());
192            }
193        }
194
195        while (entryIterator.hasNext()) {
196            final E entry = entryIterator.next();
197            boolean copy = true;
198
199            for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
200                final Change<E> change = it.next();
201
202                final ChangeType type = change.getType();
203                final String name = entry.getName();
204                if (type == ChangeType.DELETE && name != null) {
205                    if (name.equals(change.getTargetFileName())) {
206                        copy = false;
207                        it.remove();
208                        results.deleted(name);
209                        break;
210                    }
211                } else if (type == ChangeType.DELETE_DIR && name != null) {
212                    // don't combine ifs to make future extensions more easy
213                    if (name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
214                        copy = false;
215                        results.deleted(name);
216                        break;
217                    }
218                }
219            }
220
221            if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
222                @SuppressWarnings("resource") // InputStream not allocated here
223                final InputStream inputStream = entryIterator.getInputStream();
224                copyStream(inputStream, outputStream, entry);
225                results.addedFromStream(entry.getName());
226            }
227        }
228
229        // Adds files which hasn't been added from the original and do not have replace mode on
230        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
231            final Change<E> change = it.next();
232
233            if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
234                @SuppressWarnings("resource")
235                final InputStream input = change.getInputStream();
236                copyStream(input, outputStream, change.getEntry());
237                it.remove();
238                results.addedFromChangeSet(change.getEntry().getName());
239            }
240        }
241        outputStream.finish();
242        return results;
243    }
244
245    /**
246     * 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.
247     *
248     * This method finishes the stream, no other entries should be added after that.
249     *
250     * @param inputStream  the InputStream to perform the changes on
251     * @param outputStream the resulting OutputStream with all modifications
252     * @throws IOException if a read/write error occurs
253     * @return the results of this operation
254     */
255    public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
256        return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
257    }
258
259    /**
260     * 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.
261     *
262     * This method finishes the stream, no other entries should be added after that.
263     *
264     * @param zipFile      the ZipFile to perform the changes on
265     * @param outputStream the resulting OutputStream with all modifications
266     * @throws IOException if a read/write error occurs
267     * @return the results of this operation
268     * @since 1.5
269     */
270    public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
271        @SuppressWarnings("unchecked")
272        final ArchiveEntryIterator<E> entryIterator = (ArchiveEntryIterator<E>) new ZipFileIterator(zipFile);
273        return perform(entryIterator, outputStream);
274    }
275}