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;
034
035/**
036 * 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
037 * changes, a new Performer must be created.
038 *
039 * @param <I> The {@link ArchiveInputStream} type.
040 * @param <O> The {@link ArchiveOutputStream} type.
041 * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
042 * @ThreadSafe
043 * @Immutable
044 */
045public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {
046
047    /**
048     * Abstracts getting entries and streams for archive entries.
049     *
050     * <p>
051     * 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
052     * exceptions thrown in ArchiveInputStream#getNextEntry.
053     * </p>
054     */
055    private interface ArchiveEntryIterator<E extends ArchiveEntry> {
056
057        InputStream getInputStream() throws IOException;
058
059        boolean hasNext() throws IOException;
060
061        E next();
062    }
063
064    private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {
065
066        private final ArchiveInputStream<E> inputStream;
067        private E next;
068
069        ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
070            this.inputStream = inputStream;
071        }
072
073        @Override
074        public InputStream getInputStream() {
075            return inputStream;
076        }
077
078        @Override
079        public boolean hasNext() throws IOException {
080            return (next = inputStream.getNextEntry()) != null;
081        }
082
083        @Override
084        public E next() {
085            return next;
086        }
087    }
088
089    private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {
090
091        private final ZipFile zipFile;
092        private final Enumeration<ZipArchiveEntry> nestedEnumeration;
093        private ZipArchiveEntry currentEntry;
094
095        ZipFileIterator(final ZipFile zipFile) {
096            this.zipFile = zipFile;
097            this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
098        }
099
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}